diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c57e37f938..1134b9dc29 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,6 +26,7 @@ env: common desktop desktop-package + foundations qms-desktop-package dev models @@ -265,7 +266,7 @@ jobs: ./profile-start.sh - name: Run UI tests run: | - cd ./tests/sanity + cd ./tests/sanity node ../../common/scripts/install-run-rushx.js uitest - name: Download profile run: | diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 675c363588..75f1ee8847 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -130,11 +130,11 @@ importers: specifier: workspace:^0.7.0 version: link:../plugins/chunter-resources '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../foundations/core/packages/client-resources '@hcengineering/communication': specifier: workspace:^0.7.0 version: link:../plugins/communication @@ -145,8 +145,8 @@ importers: specifier: workspace:^0.7.0 version: link:../plugins/communication-resources '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../plugins/contact @@ -166,8 +166,8 @@ importers: specifier: workspace:^0.7.0 version: link:../plugins/controlled-documents-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../foundations/core/packages/core '@hcengineering/desktop-downloads': specifier: workspace:^0.7.0 version: link:../plugins/desktop-downloads @@ -370,8 +370,8 @@ importers: specifier: workspace:^0.7.0 version: link:../plugins/onboard-resources '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../plugins/preference @@ -616,8 +616,8 @@ importers: specifier: workspace:^0.7.0 version: link:../plugins/workbench-resources commander: - specifier: ^8.1.0 - version: 8.3.0 + specifier: ^14.0.0 + version: 14.0.2 dotenv: specifier: ^16.4.5 version: 16.6.1 @@ -650,8 +650,8 @@ importers: version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../foundations/utils/packages/platform-rig '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.9.1 @@ -659,10 +659,10 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -698,7 +698,7 @@ importers: specifier: ^38.2.2 version: 38.7.1 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 esbuild-loader: specifier: ^4.3.0 @@ -776,7 +776,7 @@ importers: specifier: ^9.2.5 version: 9.5.4(typescript@5.9.3)(webpack@5.103.0) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) ts-node-dev: specifier: ^2.0.0 @@ -816,10 +816,10 @@ importers: specifier: workspace:^0.7.0 version: link:../desktop '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../foundations/utils/packages/platform-rig '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -856,7 +856,7 @@ importers: version: 6.6.0(eslint@8.57.1) node-loader: specifier: ~2.0.0 - version: 2.0.0(webpack@5.103.0(@swc/core@1.15.3)) + version: 2.0.0(webpack@5.103.0) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -870,8 +870,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/attachment '@hcengineering/collaborator-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/collaborator-client '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact @@ -879,29 +879,29 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/controlled-documents '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/server-tool': specifier: workspace:^0.7.0 version: link:../../server/tool commander: - specifier: ^8.1.0 - version: 8.3.0 + specifier: ^14.0.0 + version: 14.0.2 docx4js: specifier: ^3.2.20 version: 3.3.0 @@ -922,8 +922,8 @@ importers: version: 3.25.76 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/domhandler': specifier: ^2.4.5 version: 2.4.5 @@ -937,7 +937,7 @@ importers: specifier: ~7.0.11 version: 7.0.18 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -949,7 +949,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -976,7 +976,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -985,20 +985,20 @@ importers: ../../dev/import-tool: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/importer': specifier: workspace:^0.7.0 version: link:../../packages/importer '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client commander: - specifier: ^8.1.0 - version: 8.3.0 + specifier: ^14.0.0 + version: 14.0.2 js-yaml: specifier: ^4.1.0 version: 4.1.1 @@ -1007,8 +1007,8 @@ importers: version: 1.11.0 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -1016,7 +1016,7 @@ importers: specifier: ^4.0.9 version: 4.0.9 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -1028,7 +1028,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -1055,7 +1055,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -1097,8 +1097,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/ai-bot-resources '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-collector': specifier: workspace:^0.7.0 version: link:../../plugins/analytics-collector @@ -1184,11 +1184,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/chunter-resources '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/communication': specifier: workspace:^0.7.0 version: link:../../plugins/communication @@ -1217,8 +1217,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/controlled-documents-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/desktop-preferences': specifier: workspace:^0.7.0 version: link:../../plugins/desktop-preferences @@ -1418,8 +1418,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/onboard-resources '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../plugins/preference @@ -1782,10 +1782,10 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 autoprefixer: specifier: ^10.4.14 @@ -1806,7 +1806,7 @@ importers: specifier: ^8.0.1 version: 8.1.1(webpack@5.103.0) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 esbuild-loader: specifier: ^4.3.0 @@ -1884,8 +1884,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../server/account '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/account-service': specifier: workspace:^0.7.0 version: link:../../server/account-service @@ -1893,8 +1893,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/activity '@hcengineering/api-client': - specifier: ^0.7.18 - version: 0.7.18(bufferutil@4.0.9)(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/api-client '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../plugins/attachment @@ -1914,20 +1914,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/chunter '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/collaboration': - specifier: ^0.7.16 - version: 0.7.16(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/collaboration '@hcengineering/communication': specifier: workspace:^0.7.0 version: link:../../plugins/communication '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact @@ -1935,23 +1935,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/controlled-documents '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/datalake': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/datalake '@hcengineering/document': specifier: workspace:^0.7.0 version: link:../../plugins/document '@hcengineering/elastic': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/elastic '@hcengineering/hulylake-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/hulylake-client '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/server/packages/kafka '@hcengineering/kvs-client': specifier: workspace:^0.7.0 version: link:../../packages/kvs-client @@ -1959,11 +1959,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/lead '@hcengineering/minio': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/minio '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-activity': specifier: workspace:^0.7.0 version: link:../../models/activity @@ -2001,20 +2001,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../models/tracker '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/mongo '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/pod-rating': specifier: workspace:^0.7.0 version: link:../../services/rating '@hcengineering/postgres': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/server/packages/postgres '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/recruit': specifier: workspace:^0.7.0 version: link:../../plugins/recruit @@ -2022,11 +2022,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/rekoni '@hcengineering/retry': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/retry '@hcengineering/s3': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/s3 '@hcengineering/server-activity': specifier: workspace:^0.7.0 version: link:../../server-plugins/activity @@ -2067,8 +2067,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../server-plugins/chunter-resources '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-collaboration': specifier: workspace:^0.7.0 version: link:../../server-plugins/collaboration @@ -2082,8 +2082,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../server-plugins/contact-resources '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-document': specifier: workspace:^0.7.0 version: link:../../server-plugins/document @@ -2154,8 +2154,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../server-plugins/setting-resources '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-tags': specifier: workspace:^0.7.0 version: link:../../server-plugins/tags @@ -2181,8 +2181,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../server-plugins/time-resources '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/server-tool': specifier: workspace:^0.7.0 version: link:../../server/tool @@ -2211,17 +2211,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/telegram '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-core': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text-core '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/text-markdown '@hcengineering/text-ydoc': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text-ydoc '@hcengineering/tracker': specifier: workspace:^0.7.0 version: link:../../plugins/tracker @@ -2232,8 +2232,8 @@ importers: specifier: ^4.0.8 version: 4.0.9 commander: - specifier: ^8.1.0 - version: 8.3.0 + specifier: ^14.0.0 + version: 14.0.2 csv-parse: specifier: ~5.1.0 version: 5.1.0 @@ -2269,8 +2269,8 @@ importers: version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -2284,13 +2284,13 @@ importers: specifier: ~7.0.11 version: 7.0.18 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/request': specifier: ~2.48.8 version: 2.48.13 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -2302,7 +2302,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -2329,63 +2329,115 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 - ../../models/achievement: + ../../foundations/communication/packages/client-query: dependencies: - '@hcengineering/achievement': - specifier: workspace:^0.7.0 - version: link:../../plugins/achievement - '@hcengineering/achievement-resources': - specifier: workspace:^0.7.0 - version: link:../../plugins/achievement-resources - '@hcengineering/contact': - specifier: workspace:^0.7.0 - version: link:../../plugins/contact - '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 - '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 - '@hcengineering/model-core': - specifier: workspace:^0.7.0 - version: link:../core - '@hcengineering/model-presentation': - specifier: workspace:^0.7.0 - version: link:../presentation - '@hcengineering/model-view': - specifier: workspace:^0.7.0 - version: link:../view - '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 - '@hcengineering/presentation': - specifier: workspace:^0.7.0 - version: link:../../packages/presentation - '@hcengineering/ui': - specifier: workspace:^0.7.0 - version: link:../../packages/ui + '@hcengineering/communication-query': + specifier: workspace:^0.7.11 + version: link:../query + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + '@hcengineering/hulylake-client': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/hulylake-client + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + esbuild-plugin-copy: + specifier: ^2.1.1 + version: 2.1.1(esbuild@0.25.12) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/communication/packages/cockroach: + dependencies: + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-shared': + specifier: workspace:^0.7.11 + version: link:../shared + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + postgres: + specifier: ^3.4.7 + version: 3.4.7 + uuid: + specifier: ^8.3.2 + version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 + '@types/uuid': + specifier: ^8.3.1 + version: 8.3.4 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + esbuild-plugin-copy: + specifier: ^2.1.1 + version: 2.1.1(esbuild@0.25.12) eslint: specifier: ^8.54.0 version: 8.57.1 @@ -2407,73 +2459,116 @@ importers: prettier: specifier: ^3.6.2 version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 - ../../models/activity: + ../../foundations/communication/packages/query: dependencies: - '@hcengineering/activity': - specifier: workspace:^0.7.0 - version: link:../../plugins/activity - '@hcengineering/activity-resources': - specifier: workspace:^0.7.0 - version: link:../../plugins/activity-resources - '@hcengineering/contact': - specifier: workspace:^0.7.0 - version: link:../../plugins/contact + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-shared': + specifier: workspace:^0.7.11 + version: link:../shared + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + '@hcengineering/hulylake-client': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/hulylake-client + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + uuid: + specifier: ^8.3.2 + version: 8.3.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/uuid': + specifier: ^8.3.1 + version: 8.3.4 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + esbuild-plugin-copy: + specifier: ^2.1.1 + version: 2.1.1(esbuild@0.25.12) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/communication/packages/rest-client: + dependencies: + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-shared': + specifier: workspace:^0.7.11 + version: link:../shared + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 - '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 - '@hcengineering/model-core': - specifier: workspace:^0.7.0 - version: link:../core - '@hcengineering/model-preference': - specifier: workspace:^0.7.0 - version: link:../preference - '@hcengineering/model-presentation': - specifier: workspace:^0.7.0 - version: link:../presentation - '@hcengineering/model-view': - specifier: workspace:^0.7.0 - version: link:../view - '@hcengineering/notification': - specifier: workspace:^0.7.0 - version: link:../../plugins/notification - '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 - '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) - '@hcengineering/ui': - specifier: workspace:^0.7.0 - version: link:../../packages/ui - '@hcengineering/view': - specifier: workspace:^0.7.0 - version: link:../../plugins/view + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + snappyjs: + specifier: ^0.7.0 + version: 0.7.0 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.19.1 + '@types/snappyjs': + specifier: ^0.7.1 + version: 0.7.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + esbuild-plugin-copy: + specifier: ^2.1.1 + version: 2.1.1(esbuild@0.25.12) eslint: specifier: ^8.54.0 version: 8.57.1 @@ -2495,55 +2590,125 @@ importers: prettier: specifier: ^3.6.2 version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 - ../../models/ai-assistant: + ../../foundations/communication/packages/sdk-types: dependencies: - '@hcengineering/ai-assistant': - specifier: workspace:^0.7.0 - version: link:../../plugins/ai-assistant - '@hcengineering/ai-assistant-resources': - specifier: workspace:^0.7.0 - version: link:../../plugins/ai-assistant-resources + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 - '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 - '@hcengineering/model-core': - specifier: workspace:^0.7.0 - version: link:../core - '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 - '@hcengineering/setting': - specifier: workspace:^0.7.0 - version: link:../../plugins/setting - '@hcengineering/ui': - specifier: workspace:^0.7.0 - version: link:../../packages/ui + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + esbuild-plugin-copy: + specifier: ^2.1.1 + version: 2.1.1(esbuild@0.25.12) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/communication/packages/server: + dependencies: + '@hcengineering/account-client': + specifier: workspace:^0.7.19 + version: link:../../../core/packages/account-client + '@hcengineering/communication-cockroach': + specifier: workspace:^0.7.11 + version: link:../cockroach + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-shared': + specifier: workspace:^0.7.11 + version: link:../shared + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/hulylake-client': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/hulylake-client + '@hcengineering/server-token': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/token + '@hcengineering/text-core': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/text-core + '@hcengineering/text-markdown': + specifier: workspace:^0.7.20 + version: link:../../../core/packages/text-markdown + uuid: + specifier: ^8.3.2 + version: 8.3.2 + zod: + specifier: ^3.22.4 + version: 3.25.76 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 + '@types/uuid': + specifier: ^8.3.1 + version: 8.3.4 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + esbuild-plugin-copy: + specifier: ^2.1.1 + version: 2.1.1(esbuild@0.25.12) eslint: specifier: ^8.54.0 version: 8.57.1 @@ -2565,9 +2730,3111 @@ importers: prettier: specifier: ^3.6.2 version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/communication/packages/shared: + dependencies: + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + '@hcengineering/hulylake-client': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/hulylake-client + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + esbuild-plugin-copy: + specifier: ^2.1.1 + version: 2.1.1(esbuild@0.25.12) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/communication/packages/types: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + esbuild-plugin-copy: + specifier: ^2.1.1 + version: 2.1.1(esbuild@0.25.12) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/account-client: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/analytics: + dependencies: + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/analytics-service: + dependencies: + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../analytics + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/measurements-otlp': + specifier: workspace:^0.7.17 + version: link:../measurements-otlp + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + winston: + specifier: ^3.11.0 + version: 3.18.3 + winston-daily-rotate-file: + specifier: ^5.0.0 + version: 5.0.0(winston@3.18.3) + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/api-client: + dependencies: + '@hcengineering/account-client': + specifier: workspace:^0.7.19 + version: link:../account-client + '@hcengineering/client': + specifier: workspace:^0.7.17 + version: link:../client + '@hcengineering/client-resources': + specifier: workspace:^0.7.17 + version: link:../client-resources + '@hcengineering/collaborator-client': + specifier: workspace:^0.7.17 + version: link:../collaborator-client + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + '@hcengineering/text': + specifier: workspace:^0.7.18 + version: link:../text + '@hcengineering/text-markdown': + specifier: workspace:^0.7.20 + version: link:../text-markdown + snappyjs: + specifier: ^0.7.0 + version: 0.7.0 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@types/snappyjs': + specifier: ^0.7.1 + version: 0.7.1 + '@types/ws': + specifier: ^8.5.12 + version: 8.18.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + optionalDependencies: + ws: + specifier: ^8.18.2 + version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) + + ../../foundations/core/packages/client: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/client-resources: + dependencies: + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../analytics + '@hcengineering/client': + specifier: workspace:^0.7.17 + version: link:../client + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + '@hcengineering/rpc': + specifier: workspace:^0.7.17 + version: link:../rpc + snappyjs: + specifier: ^0.7.0 + version: 0.7.0 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@types/snappyjs': + specifier: ^0.7.1 + version: 0.7.1 + '@types/ws': + specifier: ^8.5.12 + version: 8.18.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + ws: + specifier: ^8.18.2 + version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) + + ../../foundations/core/packages/collaborator-client: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/core: + dependencies: + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../analytics + '@hcengineering/measurements': + specifier: workspace:^0.7.18 + version: link:../measurements + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/hulylake-client: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/retry': + specifier: workspace:^0.7.17 + version: link:../retry + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/measurements: + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-node: + specifier: ^11.1.0 + version: 11.1.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + + ../../foundations/core/packages/measurements-otlp: + dependencies: + '@hcengineering/measurements': + specifier: workspace:^0.7.18 + version: link:../measurements + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/api-logs': + specifier: ^0.203.0 + version: 0.203.0 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.62.0 + version: 0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13) + '@opentelemetry/core': + specifier: ^2.0.1 + version: 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/id-generator-aws-xray': + specifier: ^2.0.0 + version: 2.0.3(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^2.0.1 + version: 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^2.0.1 + version: 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^2.0.1 + version: 2.2.0(@opentelemetry/api@1.9.0) + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-node: + specifier: ^11.1.0 + version: 11.1.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + + ../../foundations/core/packages/model: + dependencies: + '@hcengineering/account-client': + specifier: workspace:^0.7.19 + version: link:../account-client + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../analytics + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + '@hcengineering/rank': + specifier: workspace:^0.7.17 + version: link:../rank + '@hcengineering/storage': + specifier: workspace:^0.7.17 + version: link:../storage + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + toposort: + specifier: ^2.0.2 + version: 2.0.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@types/toposort': + specifier: ^2.0.3 + version: 2.0.7 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/platform: + dependencies: + intl-messageformat: + specifier: ^10.7.14 + version: 10.7.18 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/postgres-base: + dependencies: + postgres: + specifier: ^3.4.7 + version: 3.4.7 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-node: + specifier: ^11.1.0 + version: 11.1.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) + + ../../foundations/core/packages/query: + dependencies: + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../analytics + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/rank: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + lexorank: + specifier: ~1.0.4 + version: 1.0.5 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/retry: + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/rpc: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + msgpackr: + specifier: ^1.11.2 + version: 1.11.5 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/storage: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/storage-client: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/text: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/text-core': + specifier: workspace:^0.7.18 + version: link:../text-core + '@tiptap/core': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extension-blockquote': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-bold': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-bullet-list': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-code': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-code-block': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + '@tiptap/extension-document': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-dropcursor': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + '@tiptap/extension-gapcursor': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + '@tiptap/extension-hard-break': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-heading': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-highlight': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-history': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + '@tiptap/extension-horizontal-rule': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + '@tiptap/extension-italic': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-link': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + '@tiptap/extension-list-item': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-mention': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(@tiptap/suggestion@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)) + '@tiptap/extension-ordered-list': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-paragraph': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-strike': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-table': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + '@tiptap/extension-table-cell': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-table-header': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-table-row': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-task-item': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + '@tiptap/extension-task-list': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-text': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-text-align': + specifier: ~2.11.0 + version: 2.11.9(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-text-style': + specifier: ~2.11.0 + version: 2.11.9(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-typography': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extension-underline': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/html': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + '@tiptap/pm': + specifier: ^2.11.7 + version: 2.27.1 + '@tiptap/starter-kit': + specifier: ^2.11.7 + version: 2.27.1 + '@tiptap/suggestion': + specifier: ^2.11.7 + version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + prosemirror-codemark: + specifier: ^0.4.2 + version: 0.4.2(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/markdown-it': + specifier: ~13.0.0 + version: 13.0.9 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/text-core: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + hash-it: + specifier: ^6.0.0 + version: 6.0.1 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/markdown-it': + specifier: ~13.0.0 + version: 13.0.9 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest-environment-jsdom: + specifier: ^30.2.0 + version: 30.2.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/text-html: + dependencies: + '@hcengineering/text-core': + specifier: workspace:^0.7.18 + version: link:../text-core + htmlparser2: + specifier: ^9.0.0 + version: 9.1.0 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/text-markdown: + dependencies: + '@hcengineering/text-core': + specifier: workspace:^0.7.18 + version: link:../text-core + '@hcengineering/text-html': + specifier: workspace:^0.7.18 + version: link:../text-html + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + markdown-it: + specifier: ^14.0.0 + version: 14.1.0 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/markdown-it': + specifier: ~13.0.0 + version: 13.0.9 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/core/packages/text-ydoc: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/text': + specifier: workspace:^0.7.18 + version: link:../text + '@hcengineering/text-core': + specifier: workspace:^0.7.18 + version: link:../text-core + y-protocols: + specifier: ^1.0.6 + version: 1.0.6(yjs@13.6.27) + yjs: + specifier: ^13.6.27 + version: 13.6.27 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest-environment-jsdom: + specifier: ^30.2.0 + version: 30.2.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + y-prosemirror: + specifier: ^1.3.7 + version: 1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + + ../../foundations/core/packages/token: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + jwt-simple: + specifier: ^0.5.6 + version: 0.5.6 + uuid: + specifier: ^8.3.2 + version: 8.3.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@types/uuid': + specifier: ^8.3.1 + version: 8.3.4 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/client: + dependencies: + '@hcengineering/account-client': + specifier: workspace:^0.7.19 + version: link:../../../core/packages/account-client + '@hcengineering/client': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/client + '@hcengineering/client-resources': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/client-resources + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + '@hcengineering/server-token': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/token + ws: + specifier: ^8.18.2 + version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@types/uuid': + specifier: ^8.3.1 + version: 8.3.4 + '@types/ws': + specifier: ^8.5.12 + version: 8.18.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/collaboration: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + '@hcengineering/text': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/text + '@hcengineering/text-ydoc': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/text-ydoc + base64-js: + specifier: ^1.5.1 + version: 1.5.1 + yjs: + specifier: ^13.6.27 + version: 13.6.27 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/core: + dependencies: + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/analytics + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/query': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/query + '@hcengineering/rpc': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/rpc + '@hcengineering/server-token': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/token + '@hcengineering/storage': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/storage + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + uuid: + specifier: ^8.3.2 + version: 8.3.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@types/uuid': + specifier: ^8.3.1 + version: 8.3.4 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/datalake: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + '@hcengineering/server-token': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/token + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/elastic: + dependencies: + '@elastic/elasticsearch': + specifier: ^7.17.14 + version: 7.17.14 + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/analytics + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/hulylake: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/hulylake-client': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/hulylake-client + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + '@hcengineering/server-token': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/token + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/kafka: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + '@hcengineering/storage': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/storage + kafkajs: + specifier: ^2.2.4 + version: 2.2.4 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/middleware: + dependencies: + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/analytics + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/query': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/query + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + fast-equals: + specifier: ^5.2.2 + version: 5.3.3 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/minio: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + minio: + specifier: ^8.0.5 + version: 8.0.6 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/mongo: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + bson: + specifier: ^6.10.3 + version: 6.10.4 + mongodb: + specifier: ^6.16.0 + version: 6.21.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/postgres: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/postgres-base': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/postgres-base + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + postgres: + specifier: ^3.4.7 + version: 3.4.7 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/s3: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.738.0 + version: 3.937.0 + '@aws-sdk/lib-storage': + specifier: ^3.738.0 + version: 3.937.0(@aws-sdk/client-s3@3.937.0) + '@aws-sdk/s3-request-presigner': + specifier: ^3.738.0 + version: 3.937.0 + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + '@hcengineering/storage': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/storage + '@smithy/node-http-handler': + specifier: ^4.0.2 + version: 4.4.5 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/server: + dependencies: + '@hcengineering/account-client': + specifier: workspace:^0.7.19 + version: link:../../../core/packages/account-client + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/analytics + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/rpc': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/rpc + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + '@hcengineering/server-token': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/token + utf-8-validate: + specifier: ^6.0.4 + version: 6.0.5 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/server/packages/server-storage: + dependencies: + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/analytics + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../../core/packages/core + '@hcengineering/datalake': + specifier: workspace:^0.7.16 + version: link:../datalake + '@hcengineering/hulylake': + specifier: workspace:^0.7.16 + version: link:../hulylake + '@hcengineering/minio': + specifier: workspace:^0.7.16 + version: link:../minio + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../../core/packages/platform + '@hcengineering/s3': + specifier: workspace:^0.7.16 + version: link:../s3 + '@hcengineering/server-core': + specifier: workspace:^0.7.17 + version: link:../core + '@hcengineering/server-token': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/token + '@hcengineering/storage': + specifier: workspace:^0.7.17 + version: link:../../../core/packages/storage + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../../utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/utils/packages/platform-rig: + dependencies: + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.10 + version: 0.25.12 + esbuild-plugin-copy: + specifier: ^2.1.1 + version: 2.1.1(esbuild@0.25.12) + esbuild-svelte: + specifier: ^0.9.3 + version: 0.9.3(esbuild@0.25.12)(svelte@4.2.20) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + prettier-plugin-svelte: + specifier: ^3.4.0 + version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) + svelte: + specifier: ^4.2.20 + version: 4.2.20 + svelte-eslint-parser: + specifier: ^0.33.1 + version: 0.33.1(svelte@4.2.20) + svelte-preprocess: + specifier: ^5.1.4 + version: 5.1.4(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(svelte@4.2.20)(typescript@5.9.3) + svelte2tsx: + specifier: ^0.7.45 + version: 0.7.45(svelte@4.2.20)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../foundations/utils/packages/ui-test: + dependencies: + svelte: + specifier: ^4.2.20 + version: 4.2.20 + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-node: + specifier: ^11.1.0 + version: 11.1.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + sass: + specifier: ^1.80.0 + version: 1.94.2 + svelte-loader: + specifier: ^3.2.0 + version: 3.2.4(svelte@4.2.20) + svelte-preprocess: + specifier: ^5.1.4 + version: 5.1.4(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(svelte@4.2.20)(typescript@5.9.3) ts-jest: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + + ../../models/achievement: + dependencies: + '@hcengineering/achievement': + specifier: workspace:^0.7.0 + version: link:../../plugins/achievement + '@hcengineering/achievement-resources': + specifier: workspace:^0.7.0 + version: link:../../plugins/achievement-resources + '@hcengineering/contact': + specifier: workspace:^0.7.0 + version: link:../../plugins/contact + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core + '@hcengineering/model': + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model + '@hcengineering/model-core': + specifier: workspace:^0.7.0 + version: link:../core + '@hcengineering/model-presentation': + specifier: workspace:^0.7.0 + version: link:../presentation + '@hcengineering/model-view': + specifier: workspace:^0.7.0 + version: link:../view + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform + '@hcengineering/presentation': + specifier: workspace:^0.7.0 + version: link:../../packages/presentation + '@hcengineering/ui': + specifier: workspace:^0.7.0 + version: link:../../packages/ui + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../models/activity: + dependencies: + '@hcengineering/activity': + specifier: workspace:^0.7.0 + version: link:../../plugins/activity + '@hcengineering/activity-resources': + specifier: workspace:^0.7.0 + version: link:../../plugins/activity-resources + '@hcengineering/contact': + specifier: workspace:^0.7.0 + version: link:../../plugins/contact + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core + '@hcengineering/model': + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model + '@hcengineering/model-core': + specifier: workspace:^0.7.0 + version: link:../core + '@hcengineering/model-preference': + specifier: workspace:^0.7.0 + version: link:../preference + '@hcengineering/model-presentation': + specifier: workspace:^0.7.0 + version: link:../presentation + '@hcengineering/model-view': + specifier: workspace:^0.7.0 + version: link:../view + '@hcengineering/notification': + specifier: workspace:^0.7.0 + version: link:../../plugins/notification + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform + '@hcengineering/text': + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text + '@hcengineering/ui': + specifier: workspace:^0.7.0 + version: link:../../packages/ui + '@hcengineering/view': + specifier: workspace:^0.7.0 + version: link:../../plugins/view + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../models/ai-assistant: + dependencies: + '@hcengineering/ai-assistant': + specifier: workspace:^0.7.0 + version: link:../../plugins/ai-assistant + '@hcengineering/ai-assistant-resources': + specifier: workspace:^0.7.0 + version: link:../../plugins/ai-assistant-resources + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core + '@hcengineering/model': + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model + '@hcengineering/model-core': + specifier: workspace:^0.7.0 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform + '@hcengineering/setting': + specifier: workspace:^0.7.0 + version: link:../../plugins/setting + '@hcengineering/ui': + specifier: workspace:^0.7.0 + version: link:../../packages/ui + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -2578,29 +5845,29 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/ai-bot '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -2631,7 +5898,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -2639,11 +5906,11 @@ importers: ../../models/all: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-achievement': specifier: workspace:^0.7.0 version: link:../achievement @@ -2924,20 +6191,20 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-github-model': specifier: workspace:^0.7.0 version: link:../../services/github/server-github-model devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -2946,7 +6213,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -2972,255 +6239,255 @@ importers: ts-jest: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) - ts-node: - specifier: ^10.8.0 - version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../models/analytics-collector: - dependencies: - '@hcengineering/activity': - specifier: workspace:^0.7.0 - version: link:../../plugins/activity - '@hcengineering/analytics-collector': - specifier: workspace:^0.7.0 - version: link:../../plugins/analytics-collector - '@hcengineering/attachment': - specifier: workspace:^0.7.0 - version: link:../../plugins/attachment - '@hcengineering/chunter': - specifier: workspace:^0.7.0 - version: link:../../plugins/chunter - '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 - '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 - '@hcengineering/model-activity': - specifier: workspace:^0.7.0 - version: link:../activity - '@hcengineering/model-chunter': - specifier: workspace:^0.7.0 - version: link:../chunter - '@hcengineering/model-core': - specifier: workspace:^0.7.0 - version: link:../core - '@hcengineering/model-notification': - specifier: workspace:^0.7.0 - version: link:../notification - '@hcengineering/model-view': - specifier: workspace:^0.7.0 - version: link:../view - '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 - '@hcengineering/ui': - specifier: workspace:^0.7.0 - version: link:../../packages/ui - '@hcengineering/view': - specifier: workspace:^0.7.0 - version: link:../../plugins/view - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.19.1 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) - prettier: - specifier: ^3.6.2 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../models/attachment: - dependencies: - '@hcengineering/activity': - specifier: workspace:^0.7.0 - version: link:../../plugins/activity - '@hcengineering/attachment': - specifier: workspace:^0.7.0 - version: link:../../plugins/attachment - '@hcengineering/attachment-resources': - specifier: workspace:^0.7.0 - version: link:../../plugins/attachment-resources - '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 - '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 - '@hcengineering/model-core': - specifier: workspace:^0.7.0 - version: link:../core - '@hcengineering/model-preference': - specifier: workspace:^0.7.0 - version: link:../preference - '@hcengineering/model-presentation': - specifier: workspace:^0.7.0 - version: link:../presentation - '@hcengineering/model-uploader': - specifier: workspace:^0.7.0 - version: link:../uploader - '@hcengineering/model-view': - specifier: workspace:^0.7.0 - version: link:../view - '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 - '@hcengineering/ui': - specifier: workspace:^0.7.0 - version: link:../../packages/ui - '@hcengineering/view': - specifier: workspace:^0.7.0 - version: link:../../plugins/view - '@hcengineering/workbench': - specifier: workspace:^0.7.0 - version: link:../../plugins/workbench - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.19.1 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) - prettier: - specifier: ^3.6.2 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../models/billing: - dependencies: - '@hcengineering/billing': - specifier: workspace:^0.7.0 - version: link:../../plugins/billing - '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 - '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 - '@hcengineering/model-core': - specifier: workspace:^0.7.0 - version: link:../core - '@hcengineering/model-presentation': - specifier: workspace:^0.7.0 - version: link:../presentation - '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 - '@hcengineering/setting': - specifier: workspace:^0.7.0 - version: link:../../plugins/setting - '@hcengineering/workbench': - specifier: workspace:^0.7.0 - version: link:../../plugins/workbench - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.19.1 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) - prettier: - specifier: ^3.6.2 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../models/analytics-collector: + dependencies: + '@hcengineering/activity': + specifier: workspace:^0.7.0 + version: link:../../plugins/activity + '@hcengineering/analytics-collector': + specifier: workspace:^0.7.0 + version: link:../../plugins/analytics-collector + '@hcengineering/attachment': + specifier: workspace:^0.7.0 + version: link:../../plugins/attachment + '@hcengineering/chunter': + specifier: workspace:^0.7.0 + version: link:../../plugins/chunter + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core + '@hcengineering/model': + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model + '@hcengineering/model-activity': + specifier: workspace:^0.7.0 + version: link:../activity + '@hcengineering/model-chunter': + specifier: workspace:^0.7.0 + version: link:../chunter + '@hcengineering/model-core': + specifier: workspace:^0.7.0 + version: link:../core + '@hcengineering/model-notification': + specifier: workspace:^0.7.0 + version: link:../notification + '@hcengineering/model-view': + specifier: workspace:^0.7.0 + version: link:../view + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform + '@hcengineering/ui': + specifier: workspace:^0.7.0 + version: link:../../packages/ui + '@hcengineering/view': + specifier: workspace:^0.7.0 + version: link:../../plugins/view + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../models/attachment: + dependencies: + '@hcengineering/activity': + specifier: workspace:^0.7.0 + version: link:../../plugins/activity + '@hcengineering/attachment': + specifier: workspace:^0.7.0 + version: link:../../plugins/attachment + '@hcengineering/attachment-resources': + specifier: workspace:^0.7.0 + version: link:../../plugins/attachment-resources + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core + '@hcengineering/model': + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model + '@hcengineering/model-core': + specifier: workspace:^0.7.0 + version: link:../core + '@hcengineering/model-preference': + specifier: workspace:^0.7.0 + version: link:../preference + '@hcengineering/model-presentation': + specifier: workspace:^0.7.0 + version: link:../presentation + '@hcengineering/model-uploader': + specifier: workspace:^0.7.0 + version: link:../uploader + '@hcengineering/model-view': + specifier: workspace:^0.7.0 + version: link:../view + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform + '@hcengineering/ui': + specifier: workspace:^0.7.0 + version: link:../../packages/ui + '@hcengineering/view': + specifier: workspace:^0.7.0 + version: link:../../plugins/view + '@hcengineering/workbench': + specifier: workspace:^0.7.0 + version: link:../../plugins/workbench + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../models/billing: + dependencies: + '@hcengineering/billing': + specifier: workspace:^0.7.0 + version: link:../../plugins/billing + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core + '@hcengineering/model': + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model + '@hcengineering/model-core': + specifier: workspace:^0.7.0 + version: link:../core + '@hcengineering/model-presentation': + specifier: workspace:^0.7.0 + version: link:../presentation + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform + '@hcengineering/setting': + specifier: workspace:^0.7.0 + version: link:../../plugins/setting + '@hcengineering/workbench': + specifier: workspace:^0.7.0 + version: link:../../plugins/workbench + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -3240,11 +6507,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -3261,8 +6528,8 @@ importers: specifier: workspace:^0.7.0 version: link:../view '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../plugins/preference @@ -3277,13 +6544,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -3314,7 +6581,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -3331,11 +6598,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -3364,8 +6631,8 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/tags': specifier: workspace:^0.7.0 version: link:../../plugins/tags @@ -3383,13 +6650,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -3420,7 +6687,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -3428,8 +6695,8 @@ importers: ../../models/calendar: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/activity': specifier: workspace:^0.7.0 version: link:../../plugins/activity @@ -3443,11 +6710,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -3476,8 +6743,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -3492,13 +6759,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -3529,7 +6796,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -3552,11 +6819,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -3582,8 +6849,8 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/tags': specifier: workspace:^0.7.0 version: link:../../plugins/tags @@ -3601,13 +6868,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -3638,7 +6905,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -3658,11 +6925,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/communication '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-card': specifier: workspace:^0.7.0 version: link:../card @@ -3676,8 +6943,8 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -3692,13 +6959,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -3729,7 +6996,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -3752,11 +7019,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-activity': specifier: workspace:^0.7.0 version: link:../activity @@ -3782,8 +7049,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -3795,13 +7062,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -3832,7 +7099,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -3849,17 +7116,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/communication-resources '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-card': specifier: workspace:^0.7.0 version: link:../card @@ -3873,8 +7140,8 @@ importers: specifier: workspace:^0.7.0 version: link:../view '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -3883,13 +7150,13 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -3920,7 +7187,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -3943,11 +7210,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-activity': specifier: workspace:^0.7.0 version: link:../activity @@ -3985,11 +7252,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -4007,13 +7274,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -4044,7 +7311,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -4061,8 +7328,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/chunter '@hcengineering/collaboration': - specifier: ^0.7.16 - version: 0.7.16(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/collaboration '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact @@ -4073,11 +7340,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/controlled-documents-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -4115,11 +7382,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/request': specifier: workspace:^0.7.0 version: link:../../plugins/request @@ -4146,13 +7413,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -4186,7 +7453,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -4194,32 +7461,32 @@ importers: ../../models/core: dependencies: '@hcengineering/collaboration': - specifier: ^0.7.16 - version: 0.7.16(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/collaboration '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/storage': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/storage '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -4250,7 +7517,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -4258,8 +7525,8 @@ importers: ../../models/desktop-downloads: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/desktop-downloads': specifier: workspace:^0.7.0 version: link:../../plugins/desktop-downloads @@ -4267,8 +7534,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/desktop-downloads-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -4279,20 +7546,20 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -4323,7 +7590,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -4331,14 +7598,14 @@ importers: ../../models/desktop-preferences: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/desktop-preferences': specifier: workspace:^0.7.0 version: link:../../plugins/desktop-preferences '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -4350,13 +7617,13 @@ importers: version: link:../../plugins/notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -4387,7 +7654,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -4401,11 +7668,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/attachment '@hcengineering/collaboration': - specifier: ^0.7.16 - version: 0.7.16(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/collaboration '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/document': specifier: workspace:^0.7.0 version: link:../../plugins/document @@ -4413,8 +7680,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/document-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-activity': specifier: workspace:^0.7.0 version: link:../activity @@ -4452,11 +7719,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -4474,13 +7741,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -4511,7 +7778,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -4525,8 +7792,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/chunter '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../../plugins/drive @@ -4534,8 +7801,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/drive-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -4558,8 +7825,8 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -4568,13 +7835,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -4605,7 +7872,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -4613,14 +7880,14 @@ importers: ../../models/emoji: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/emoji': specifier: workspace:^0.7.0 version: link:../../plugins/emoji '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -4637,8 +7904,8 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -4650,13 +7917,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -4687,7 +7954,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -4695,8 +7962,8 @@ importers: ../../models/export: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/export': specifier: workspace:^0.7.0 version: link:../../plugins/export @@ -4704,8 +7971,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/export-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -4713,8 +7980,8 @@ importers: specifier: workspace:^0.7.0 version: link:../presentation '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -4726,13 +7993,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -4763,7 +8030,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -4777,8 +8044,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/gmail': specifier: workspace:^0.7.0 version: link:../../plugins/gmail @@ -4786,8 +8053,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/gmail-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -4810,8 +8077,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../plugins/preference @@ -4826,13 +8093,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -4863,7 +8130,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -4871,8 +8138,8 @@ importers: ../../models/guest: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/guest': specifier: workspace:^0.7.0 version: link:../../plugins/guest @@ -4880,8 +8147,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/guest-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -4889,8 +8156,8 @@ importers: specifier: workspace:^0.7.0 version: link:../view '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -4899,13 +8166,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -4936,7 +8203,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -4947,8 +8214,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/hr': specifier: workspace:^0.7.0 version: link:../../plugins/hr @@ -4956,8 +8223,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/hr-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -4983,8 +8250,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -4993,13 +8260,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5030,7 +8297,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5038,8 +8305,8 @@ importers: ../../models/huly-mail: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/huly-mail': specifier: workspace:^0.7.0 version: link:../../plugins/huly-mail @@ -5047,14 +8314,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/huly-mail-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -5063,13 +8330,13 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5100,7 +8367,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5111,8 +8378,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/card '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/inbox': specifier: workspace:^0.7.0 version: link:../../plugins/inbox @@ -5120,8 +8387,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/inbox-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -5129,8 +8396,8 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -5145,13 +8412,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5182,7 +8449,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5196,8 +8463,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/chunter '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/inventory': specifier: workspace:^0.7.0 version: link:../../plugins/inventory @@ -5205,8 +8472,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/inventory-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -5223,8 +8490,8 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -5239,13 +8506,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5276,7 +8543,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5293,8 +8560,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/lead': specifier: workspace:^0.7.0 version: link:../../plugins/lead @@ -5302,8 +8569,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/lead-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -5335,8 +8602,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -5354,13 +8621,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5391,7 +8658,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5411,8 +8678,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../../plugins/drive @@ -5426,8 +8693,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/media '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-calendar': specifier: workspace:^0.7.0 version: link:../calendar @@ -5450,8 +8717,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -5469,13 +8736,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5506,7 +8773,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5526,14 +8793,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/chunter '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/mail': specifier: workspace:^0.7.0 version: link:../../plugins/mail '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-card': specifier: workspace:^0.7.0 version: link:../card @@ -5544,8 +8811,8 @@ importers: specifier: workspace:^0.7.0 version: link:../view '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -5554,13 +8821,13 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5591,7 +8858,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5599,8 +8866,8 @@ importers: ../../models/media: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/media': specifier: workspace:^0.7.0 version: link:../../plugins/media @@ -5608,8 +8875,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/media-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -5620,20 +8887,20 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5664,7 +8931,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5681,11 +8948,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-activity': specifier: workspace:^0.7.0 version: link:../activity @@ -5708,8 +8975,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../plugins/preference @@ -5727,13 +8994,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5764,7 +9031,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5772,17 +9039,17 @@ importers: ../../models/preference: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../plugins/preference @@ -5791,13 +9058,13 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5828,7 +9095,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5839,11 +9106,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -5854,8 +9121,8 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presence': specifier: workspace:^0.7.0 version: link:../../plugins/presence @@ -5867,13 +9134,13 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5904,7 +9171,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5912,17 +9179,17 @@ importers: ../../models/presentation: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -5931,13 +9198,13 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -5968,7 +9235,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5976,11 +9243,11 @@ importers: ../../models/print: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -5991,8 +9258,8 @@ importers: specifier: workspace:^0.7.0 version: link:../view '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -6010,13 +9277,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -6047,7 +9314,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6061,11 +9328,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -6085,8 +9352,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -6097,8 +9364,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/process-resources '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -6113,13 +9380,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -6150,7 +9417,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6173,11 +9440,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/controlled-documents '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -6200,8 +9467,8 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/products': specifier: workspace:^0.7.0 version: link:../../plugins/products @@ -6219,13 +9486,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -6261,7 +9528,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -6273,11 +9540,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -6288,11 +9555,11 @@ importers: specifier: workspace:^0.7.0 version: link:../view '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@hcengineering/questions': specifier: workspace:^0.7.0 version: link:../../plugins/questions @@ -6335,7 +9602,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6349,11 +9616,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -6361,8 +9628,8 @@ importers: specifier: workspace:^0.7.0 version: link:../presentation '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -6380,13 +9647,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -6417,7 +9684,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6425,14 +9692,14 @@ importers: ../../models/recorder: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../../plugins/drive '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -6440,8 +9707,8 @@ importers: specifier: workspace:^0.7.0 version: link:../presentation '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/recorder': specifier: workspace:^0.7.0 version: link:../../plugins/recorder @@ -6459,13 +9726,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -6496,7 +9763,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6516,11 +9783,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -6564,8 +9831,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/recruit': specifier: workspace:^0.7.0 version: link:../../plugins/recruit @@ -6592,13 +9859,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -6629,7 +9896,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6643,11 +9910,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-chunter': specifier: workspace:^0.7.0 version: link:../chunter @@ -6664,8 +9931,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/request': specifier: workspace:^0.7.0 version: link:../../plugins/request @@ -6677,13 +9944,13 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -6714,7 +9981,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6728,11 +9995,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/card '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-activity': specifier: workspace:^0.7.0 version: link:../activity @@ -6740,8 +10007,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-activity': specifier: workspace:^0.7.0 version: link:../../server-plugins/activity @@ -6749,20 +10016,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../server-plugins/activity-resources '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -6793,7 +10060,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6813,11 +10080,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-activity': specifier: workspace:^0.7.0 version: link:../activity @@ -6843,26 +10110,26 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-ai-bot': specifier: workspace:^0.7.0 version: link:../../server-plugins/ai-bot '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/view': specifier: workspace:^0.7.0 version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -6893,7 +10160,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6904,29 +10171,29 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/attachment '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-attachment': specifier: workspace:^0.7.0 version: link:../../server-plugins/attachment '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -6957,7 +10224,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6971,32 +10238,32 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-calendar': specifier: workspace:^0.7.0 version: link:../../server-plugins/calendar '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7027,7 +10294,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7041,29 +10308,29 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/communication '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-card': specifier: workspace:^0.7.0 version: link:../../server-plugins/card '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7094,7 +10361,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7108,35 +10375,35 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/chunter '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-chunter': specifier: workspace:^0.7.0 version: link:../../server-plugins/chunter '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7167,7 +10434,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7175,29 +10442,29 @@ importers: ../../models/server-collaboration: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-collaboration': specifier: workspace:^0.7.0 version: link:../../server-plugins/collaboration '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7228,7 +10495,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7239,20 +10506,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../../server-plugins/contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification @@ -7264,13 +10531,13 @@ importers: version: link:../../plugins/templates devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7301,7 +10568,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7315,11 +10582,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/controlled-documents '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -7327,8 +10594,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/request': specifier: workspace:^0.7.0 version: link:../../plugins/request @@ -7336,20 +10603,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../server-plugins/controlled-documents '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7383,7 +10650,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7391,29 +10658,29 @@ importers: ../../models/server-core: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7444,7 +10711,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7452,23 +10719,23 @@ importers: ../../models/server-document: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/document': specifier: workspace:^0.7.0 version: link:../../plugins/document '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-document': specifier: workspace:^0.7.0 version: link:../../server-plugins/document @@ -7480,13 +10747,13 @@ importers: version: link:../../server-plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7520,7 +10787,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7528,32 +10795,32 @@ importers: ../../models/server-drive: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../../plugins/drive '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-drive': specifier: workspace:^0.7.0 version: link:../../server-plugins/drive devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7584,7 +10851,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7595,23 +10862,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/gmail': specifier: workspace:^0.7.0 version: link:../../plugins/gmail '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-gmail': specifier: workspace:^0.7.0 version: link:../../server-plugins/gmail @@ -7620,13 +10887,13 @@ importers: version: link:../../server-plugins/notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7657,7 +10924,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7665,32 +10932,32 @@ importers: ../../models/server-guest: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/guest': specifier: workspace:^0.7.0 version: link:../../plugins/guest '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-guest': specifier: workspace:^0.7.0 version: link:../../server-plugins/guest devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7721,7 +10988,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7732,20 +10999,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/hr': specifier: workspace:^0.7.0 version: link:../../plugins/hr '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-hr': specifier: workspace:^0.7.0 version: link:../../server-plugins/hr @@ -7754,13 +11021,13 @@ importers: version: link:../../server-plugins/notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7791,7 +11058,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7799,20 +11066,20 @@ importers: ../../models/server-inventory: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/inventory': specifier: workspace:^0.7.0 version: link:../../plugins/inventory '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-inventory': specifier: workspace:^0.7.0 version: link:../../server-plugins/inventory @@ -7821,13 +11088,13 @@ importers: version: link:../../server-plugins/notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7858,7 +11125,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7869,14 +11136,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/lead': specifier: workspace:^0.7.0 version: link:../../plugins/lead '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-lead': specifier: workspace:^0.7.0 version: link:../lead @@ -7884,11 +11151,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-lead': specifier: workspace:^0.7.0 version: link:../../server-plugins/lead @@ -7897,13 +11164,13 @@ importers: version: link:../../server-plugins/notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -7934,7 +11201,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7945,14 +11212,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/love': specifier: workspace:^0.7.0 version: link:../../plugins/love '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -7960,11 +11227,11 @@ importers: specifier: workspace:^0.7.0 version: link:../love '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-love': specifier: workspace:^0.7.0 version: link:../../server-plugins/love @@ -7973,13 +11240,13 @@ importers: version: link:../../server-plugins/notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8010,7 +11277,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8024,11 +11291,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-chunter': specifier: workspace:^0.7.0 version: link:../chunter @@ -8042,23 +11309,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8089,7 +11356,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8100,35 +11367,35 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/card '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-process': specifier: workspace:^0.7.0 version: link:../process '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/process': specifier: workspace:^0.7.0 version: link:../../plugins/process '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-process': specifier: workspace:^0.7.0 version: link:../../server-plugins/process devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8162,7 +11429,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8170,32 +11437,32 @@ importers: ../../models/server-products: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/products': specifier: workspace:^0.7.0 version: link:../../plugins/products '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8229,7 +11496,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8240,11 +11507,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-recruit': specifier: workspace:^0.7.0 version: link:../recruit @@ -8252,14 +11519,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../../server-plugins/contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification @@ -8271,13 +11538,13 @@ importers: version: link:../../server-plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8308,7 +11575,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8316,11 +11583,11 @@ importers: ../../models/server-request: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -8331,11 +11598,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification @@ -8344,13 +11611,13 @@ importers: version: link:../../server-plugins/request devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8381,7 +11648,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8389,17 +11656,17 @@ importers: ../../models/server-setting: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification @@ -8417,13 +11684,13 @@ importers: version: link:../../plugins/templates devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8454,7 +11721,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8462,17 +11729,17 @@ importers: ../../models/server-tags: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-tags': specifier: workspace:^0.7.0 version: link:../../server-plugins/tags @@ -8481,13 +11748,13 @@ importers: version: link:../../plugins/tags devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8518,7 +11785,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8526,17 +11793,17 @@ importers: ../../models/server-task: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification @@ -8548,13 +11815,13 @@ importers: version: link:../../plugins/task devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8585,7 +11852,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8596,20 +11863,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification @@ -8627,13 +11894,13 @@ importers: version: link:../../plugins/templates devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8664,7 +11931,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8672,20 +11939,20 @@ importers: ../../models/server-templates: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-templates': specifier: workspace:^0.7.0 version: link:../templates '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-templates': specifier: workspace:^0.7.0 version: link:../../server-plugins/templates @@ -8694,13 +11961,13 @@ importers: version: link:../../plugins/templates devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8731,7 +11998,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8739,20 +12006,20 @@ importers: ../../models/server-time: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-time': specifier: workspace:^0.7.0 version: link:../../server-plugins/time @@ -8764,13 +12031,13 @@ importers: version: link:../../plugins/tracker devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8801,7 +12068,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8812,11 +12079,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -8827,8 +12094,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification @@ -8843,13 +12110,13 @@ importers: version: link:../../plugins/tracker devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8880,7 +12147,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8888,11 +12155,11 @@ importers: ../../models/server-training: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-training': specifier: workspace:^0.7.0 version: link:../training @@ -8900,11 +12167,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification @@ -8913,13 +12180,13 @@ importers: version: link:../../server-plugins/training devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -8953,7 +12220,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8961,32 +12228,32 @@ importers: ../../models/server-view: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-view': specifier: workspace:^0.7.0 version: link:../../server-plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -9017,7 +12284,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9031,14 +12298,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/export': specifier: workspace:^0.7.0 version: link:../../plugins/export '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -9052,8 +12319,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -9074,13 +12341,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -9111,7 +12378,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9119,11 +12386,11 @@ importers: ../../models/support: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -9131,8 +12398,8 @@ importers: specifier: workspace:^0.7.0 version: link:../preference '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/support': specifier: workspace:^0.7.0 version: link:../../plugins/support @@ -9144,13 +12411,13 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -9181,7 +12448,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9192,11 +12459,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/activity '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-chunter': specifier: workspace:^0.7.0 version: link:../chunter @@ -9210,8 +12477,8 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/survey': specifier: workspace:^0.7.0 version: link:../../plugins/survey @@ -9226,13 +12493,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -9263,7 +12530,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9271,11 +12538,11 @@ importers: ../../models/tags: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -9286,8 +12553,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/tags': specifier: workspace:^0.7.0 version: link:../../plugins/tags @@ -9302,13 +12569,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -9339,7 +12606,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9353,11 +12620,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-activity': specifier: workspace:^0.7.0 version: link:../activity @@ -9392,8 +12659,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -9414,13 +12681,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -9451,7 +12718,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9468,14 +12735,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/love': specifier: workspace:^0.7.0 version: link:../../plugins/love '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -9492,8 +12759,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -9514,13 +12781,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -9551,7 +12818,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9562,11 +12829,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/activity '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-contact': specifier: workspace:^0.7.0 version: link:../contact @@ -9583,8 +12850,8 @@ importers: specifier: workspace:^0.7.0 version: link:../view '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -9602,13 +12869,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -9639,7 +12906,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9659,11 +12926,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-activity': specifier: workspace:^0.7.0 version: link:../activity @@ -9701,8 +12968,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -9729,13 +12996,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -9766,7 +13033,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9774,20 +13041,20 @@ importers: ../../models/text-editor: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../../plugins/text-editor @@ -9799,8 +13066,8 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@hcengineering/text-editor-resources': specifier: workspace:^0.7.0 version: link:../../plugins/text-editor-resources @@ -9808,7 +13075,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -9839,7 +13106,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9859,14 +13126,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/lead': specifier: workspace:^0.7.0 version: link:../../plugins/lead '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-calendar': specifier: workspace:^0.7.0 version: link:../calendar @@ -9889,11 +13156,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/recruit': specifier: workspace:^0.7.0 version: link:../../plugins/recruit @@ -9923,13 +13190,13 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -9960,7 +13227,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9977,11 +13244,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-activity': specifier: workspace:^0.7.0 version: link:../activity @@ -10016,8 +13283,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -10047,13 +13314,13 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -10084,7 +13351,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10101,11 +13368,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -10137,11 +13404,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@hcengineering/questions': specifier: workspace:^0.7.0 version: link:../../plugins/questions @@ -10196,7 +13463,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10204,11 +13471,11 @@ importers: ../../models/uploader: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -10219,8 +13486,8 @@ importers: specifier: workspace:^0.7.0 version: link:../workbench '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -10232,13 +13499,13 @@ importers: version: link:../../plugins/uploader-resources devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -10269,7 +13536,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10277,11 +13544,11 @@ importers: ../../models/view: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -10292,8 +13559,8 @@ importers: specifier: workspace:^0.7.0 version: link:../presentation '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../plugins/preference @@ -10311,13 +13578,13 @@ importers: version: link:../../plugins/view-resources devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -10348,7 +13615,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10356,11 +13623,11 @@ importers: ../../models/workbench: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -10374,8 +13641,8 @@ importers: specifier: workspace:^0.7.0 version: link:../view '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../plugins/preference @@ -10393,13 +13660,13 @@ importers: version: link:../../plugins/workbench-resources devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -10430,7 +13697,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10438,17 +13705,17 @@ importers: ../../packages/analytics-providers: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-collector': specifier: workspace:^0.7.0 version: link:../../plugins/analytics-collector '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../presentation @@ -10460,13 +13727,13 @@ importers: version: 2.0.6 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -10478,7 +13745,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -10503,7 +13770,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10511,20 +13778,20 @@ importers: ../../packages/billing-client: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -10536,7 +13803,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -10561,7 +13828,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10582,8 +13849,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -10645,11 +13912,11 @@ importers: ../../packages/hls: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../presentation @@ -10664,8 +13931,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/dompurify': specifier: ^3.0.5 version: 3.2.0 @@ -10730,20 +13997,20 @@ importers: ../../packages/hulypulse-client: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -10755,7 +14022,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -10783,7 +14050,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10800,8 +14067,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/chunter '@hcengineering/collaboration': - specifier: ^0.7.16 - version: 0.7.16(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/collaboration '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact @@ -10809,8 +14076,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/controlled-documents '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/document': specifier: workspace:^0.7.0 version: link:../../plugins/document @@ -10821,23 +14088,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../models/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/task': specifier: workspace:^0.7.0 version: link:../../plugins/task '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/text-markdown '@hcengineering/tracker': specifier: workspace:^0.7.0 version: link:../../plugins/tracker @@ -10845,8 +14112,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/view commander: - specifier: ^8.1.0 - version: 8.3.0 + specifier: ^14.0.0 + version: 14.0.2 csvtojson: specifier: ^2.0.10 version: 2.0.14 @@ -10864,8 +14131,8 @@ importers: version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -10876,7 +14143,7 @@ importers: specifier: ~2.1.1 version: 2.1.4 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 @@ -10910,7 +14177,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10918,29 +14185,29 @@ importers: ../../packages/integration-client: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/kvs-client': specifier: workspace:^0.7.0 version: link:../kvs-client '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform uuid: specifier: ^8.3.2 version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 @@ -10955,7 +14222,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -10983,7 +14250,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10991,20 +14258,20 @@ importers: ../../packages/kanban: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../presentation '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../ui @@ -11016,8 +14283,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -11079,20 +14346,20 @@ importers: ../../packages/kvs-client: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -11104,7 +14371,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -11132,7 +14399,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11149,11 +14416,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/chunter '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presence': specifier: workspace:^0.7.0 version: link:../../plugins/presence @@ -11168,8 +14435,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -11231,20 +14498,20 @@ importers: ../../packages/payment-client: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -11256,7 +14523,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -11281,7 +14548,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11289,32 +14556,32 @@ importers: ../../packages/presentation: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/collaborator-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/collaborator-client '@hcengineering/communication-client-query': - specifier: ^0.7.11 - version: 0.7.11 + specifier: workspace:^0.7.11 + version: link:../../foundations/communication/packages/client-query '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-shared': - specifier: ^0.7.11 - version: 0.7.11 + specifier: workspace:^0.7.11 + version: link:../../foundations/communication/packages/shared '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/diffview': specifier: workspace:^0.7.0 version: link:../../plugins/diffview @@ -11322,8 +14589,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/emoji '@hcengineering/hulylake-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/hulylake-client '@hcengineering/hulypulse-client': specifier: workspace:^0.7.0 version: link:../hulypulse-client @@ -11331,20 +14598,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/query': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/query '@hcengineering/retry': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/retry '@hcengineering/storage-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/storage-client '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/theme': specifier: workspace:^0.7.0 version: link:../theme @@ -11374,8 +14641,8 @@ importers: version: 13.6.27 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.9.1 @@ -11449,15 +14716,15 @@ importers: ../../packages/rekoni: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -11490,7 +14757,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11498,18 +14765,18 @@ importers: ../../packages/theme: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform svelte: specifier: ^4.2.20 version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -11571,14 +14838,14 @@ importers: ../../packages/ui: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/theme': specifier: workspace:^0.7.0 version: link:../theme @@ -11614,8 +14881,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/dompurify': specifier: ^3.0.5 version: 3.2.0 @@ -11680,18 +14947,18 @@ importers: ../../plugins/achievement: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -11724,7 +14991,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11735,17 +15002,17 @@ importers: specifier: workspace:^0.7.0 version: link:../achievement '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -11776,7 +15043,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11787,11 +15054,11 @@ importers: specifier: workspace:^0.7.0 version: link:../achievement '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -11800,13 +15067,13 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -11878,11 +15145,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -11894,8 +15161,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -11928,7 +15195,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11939,17 +15206,17 @@ importers: specifier: workspace:^0.7.0 version: link:../activity '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -11980,7 +15247,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11991,14 +15258,14 @@ importers: specifier: workspace:^0.7.0 version: link:../activity '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/card': specifier: workspace:^0.7.0 version: link:../card '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact @@ -12006,8 +15273,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/emoji': specifier: workspace:^0.7.0 version: link:../emoji @@ -12018,8 +15285,8 @@ importers: specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -12027,8 +15294,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -12043,8 +15310,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -12106,11 +15373,11 @@ importers: ../../plugins/ai-assistant: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting @@ -12119,8 +15386,8 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -12153,7 +15420,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12164,17 +15431,17 @@ importers: specifier: workspace:^0.7.0 version: link:../ai-assistant '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -12205,7 +15472,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12213,14 +15480,14 @@ importers: ../../plugins/ai-assistant-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/ai-assistant': specifier: workspace:^0.7.0 version: link:../ai-assistant '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/card': specifier: workspace:^0.7.0 version: link:../card @@ -12231,8 +15498,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/integration-client': specifier: workspace:^0.7.0 version: link:../../packages/integration-client @@ -12243,8 +15510,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -12265,8 +15532,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -12334,24 +15601,24 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/love': specifier: workspace:^0.7.0 version: link:../love '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/view': specifier: workspace:^0.7.0 version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -12387,7 +15654,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12410,14 +15677,14 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/love': specifier: workspace:^0.7.0 version: link:../love '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -12429,8 +15696,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -12495,18 +15762,18 @@ importers: specifier: workspace:^0.7.0 version: link:../chunter '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -12542,7 +15809,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12553,17 +15820,17 @@ importers: specifier: workspace:^0.7.0 version: link:../analytics-collector '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -12597,7 +15864,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12620,11 +15887,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -12633,8 +15900,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -12696,11 +15963,11 @@ importers: ../../plugins/attachment: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -12712,8 +15979,8 @@ importers: version: link:../workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -12746,7 +16013,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12757,17 +16024,17 @@ importers: specifier: workspace:^0.7.0 version: link:../attachment '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -12798,7 +16065,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12809,8 +16076,8 @@ importers: specifier: workspace:^0.7.0 version: link:../activity '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -12818,8 +16085,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/hls': specifier: workspace:^0.7.0 version: link:../../packages/hls @@ -12830,8 +16097,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -12839,8 +16106,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../text-editor @@ -12870,8 +16137,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -12933,18 +16200,18 @@ importers: ../../plugins/billing: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -12977,7 +16244,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12988,17 +16255,17 @@ importers: specifier: workspace:^0.7.0 version: link:../billing '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -13029,7 +16296,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13037,8 +16304,8 @@ importers: ../../plugins/billing-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/billing': specifier: workspace:^0.7.0 version: link:../billing @@ -13046,8 +16313,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/billing-client '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../drive @@ -13061,8 +16328,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/payment-client '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -13086,13 +16353,13 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -13161,14 +16428,14 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/gmail': specifier: workspace:^0.7.0 version: link:../gmail '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -13189,8 +16456,8 @@ importers: version: 6.11.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -13226,7 +16493,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13237,17 +16504,17 @@ importers: specifier: workspace:^0.7.0 version: link:../bitrix '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -13278,7 +16545,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13301,8 +16568,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/lead': specifier: workspace:^0.7.0 version: link:../lead @@ -13310,8 +16577,8 @@ importers: specifier: workspace:^0.7.0 version: link:../login '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -13356,8 +16623,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -13425,11 +16692,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -13447,8 +16714,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -13481,7 +16748,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13492,17 +16759,17 @@ importers: specifier: workspace:^0.7.0 version: link:../board '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -13533,7 +16800,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13568,8 +16835,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/kanban': specifier: workspace:^0.7.0 version: link:../../packages/kanban @@ -13583,8 +16850,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -13620,8 +16887,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -13686,14 +16953,14 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -13705,8 +16972,8 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -13739,7 +17006,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13750,17 +17017,17 @@ importers: specifier: workspace:^0.7.0 version: link:../calendar '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -13791,7 +17058,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13799,11 +17066,11 @@ importers: ../../plugins/calendar-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/calendar': specifier: workspace:^0.7.0 version: link:../calendar @@ -13814,8 +17081,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/integration-client': specifier: workspace:^0.7.0 version: link:../../packages/integration-client @@ -13826,8 +17093,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -13838,8 +17105,8 @@ importers: specifier: workspace:^0.7.0 version: link:../setting-resources '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor-resources': specifier: workspace:^0.7.0 version: link:../text-editor-resources @@ -13872,8 +17139,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -13935,11 +17202,11 @@ importers: ../../plugins/card: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -13951,8 +17218,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -13985,7 +17252,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13996,17 +17263,17 @@ importers: specifier: workspace:^0.7.0 version: link:../card '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -14037,7 +17304,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14045,11 +17312,11 @@ importers: ../../plugins/card-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -14069,8 +17336,8 @@ importers: specifier: workspace:^0.7.0 version: link:../communication-resources '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact @@ -14078,8 +17345,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../login @@ -14090,8 +17357,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -14102,8 +17369,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting @@ -14117,8 +17384,8 @@ importers: specifier: workspace:^0.7.0 version: link:../tags-resources '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../text-editor @@ -14126,8 +17393,8 @@ importers: specifier: workspace:^0.7.0 version: link:../text-editor-resources '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/text-markdown '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -14157,8 +17424,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/html-to-text': specifier: ^8.1.1 version: 8.1.1 @@ -14226,15 +17493,15 @@ importers: specifier: workspace:^0.7.0 version: link:../card '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -14267,7 +17534,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14278,17 +17545,17 @@ importers: specifier: workspace:^0.7.0 version: link:../chat '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -14319,7 +17586,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14327,8 +17594,8 @@ importers: ../../plugins/chat-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/card': specifier: workspace:^0.7.0 version: link:../card @@ -14345,11 +17612,11 @@ importers: specifier: workspace:^0.7.0 version: link:../communication-resources '@hcengineering/communication-shared': - specifier: ^0.7.11 - version: 0.7.11 + specifier: workspace:^0.7.11 + version: link:../../foundations/communication/packages/shared '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact @@ -14357,26 +17624,26 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/emoji-resources': specifier: workspace:^0.7.0 version: link:../emoji-resources '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/text-markdown '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -14400,8 +17667,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -14469,14 +17736,14 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -14491,8 +17758,8 @@ importers: version: 5.3.3 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -14525,7 +17792,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14536,17 +17803,17 @@ importers: specifier: workspace:^0.7.0 version: link:../chunter '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -14577,7 +17844,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14597,8 +17864,8 @@ importers: specifier: workspace:^0.7.0 version: link:../ai-bot-resources '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -14615,8 +17882,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../login @@ -14630,8 +17897,8 @@ importers: specifier: workspace:^0.7.0 version: link:../notification-resources '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -14642,8 +17909,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../text-editor @@ -14673,8 +17940,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/html-to-text': specifier: ^8.1.1 version: 8.1.1 @@ -14742,24 +18009,24 @@ importers: specifier: workspace:^0.7.0 version: link:../card '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -14792,7 +18059,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14803,17 +18070,17 @@ importers: specifier: workspace:^0.7.0 version: link:../communication '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -14844,7 +18111,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14861,8 +18128,8 @@ importers: specifier: workspace:^0.7.0 version: link:../ai-bot-resources '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment-resources': specifier: workspace:^0.7.0 version: link:../attachment-resources @@ -14876,11 +18143,11 @@ importers: specifier: workspace:^0.7.0 version: link:../communication '@hcengineering/communication-shared': - specifier: ^0.7.11 - version: 0.7.11 + specifier: workspace:^0.7.11 + version: link:../../foundations/communication/packages/shared '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact @@ -14888,8 +18155,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/emoji': specifier: workspace:^0.7.0 version: link:../emoji @@ -14897,8 +18164,8 @@ importers: specifier: workspace:^0.7.0 version: link:../emoji-resources '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presence-resources': specifier: workspace:^0.7.0 version: link:../presence-resources @@ -14909,11 +18176,11 @@ importers: specifier: workspace:^0.7.0 version: link:../process '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../text-editor @@ -14921,8 +18188,8 @@ importers: specifier: workspace:^0.7.0 version: link:../text-editor-resources '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/text-markdown '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -14946,13 +18213,13 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -15024,11 +18291,11 @@ importers: specifier: workspace:^0.7.0 version: link:../card '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -15043,8 +18310,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -15077,7 +18344,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15088,17 +18355,17 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -15129,7 +18396,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15137,8 +18404,8 @@ importers: ../../plugins/contact-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/achievement': specifier: workspace:^0.7.0 version: link:../achievement @@ -15146,8 +18413,8 @@ importers: specifier: workspace:^0.7.0 version: link:../activity '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -15158,8 +18425,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/image-cropper': specifier: workspace:^0.7.0 version: link:../image-cropper @@ -15173,8 +18440,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -15188,8 +18455,8 @@ importers: specifier: workspace:^0.7.0 version: link:../templates '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../text-editor @@ -15219,8 +18486,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/crypto-js': specifier: ^4.2.2 version: 4.2.2 @@ -15297,17 +18564,17 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/request': specifier: workspace:^0.7.0 version: link:../request @@ -15328,8 +18595,8 @@ importers: version: 1.0.5 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -15365,7 +18632,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15376,17 +18643,17 @@ importers: specifier: workspace:^0.7.0 version: link:../controlled-documents '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -15420,7 +18687,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15428,8 +18695,8 @@ importers: ../../plugins/controlled-documents-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/activity': specifier: workspace:^0.7.0 version: link:../activity @@ -15455,8 +18722,8 @@ importers: specifier: workspace:^0.7.0 version: link:../controlled-documents '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../login @@ -15470,8 +18737,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -15479,8 +18746,8 @@ importers: specifier: workspace:^0.7.0 version: link:../print '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/request': specifier: workspace:^0.7.0 version: link:../request @@ -15494,8 +18761,8 @@ importers: specifier: workspace:^0.7.0 version: link:../tags '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../text-editor @@ -15540,8 +18807,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -15606,18 +18873,18 @@ importers: ../../plugins/desktop-downloads: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -15650,7 +18917,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15661,17 +18928,17 @@ importers: specifier: workspace:^0.7.0 version: link:../desktop-downloads '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -15702,7 +18969,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15710,14 +18977,14 @@ importers: ../../plugins/desktop-downloads-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/desktop-downloads': specifier: workspace:^0.7.0 version: link:../desktop-downloads '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -15741,13 +19008,13 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -15807,14 +19074,14 @@ importers: ../../plugins/desktop-preferences: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -15823,8 +19090,8 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -15857,7 +19124,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15868,17 +19135,17 @@ importers: specifier: workspace:^0.7.0 version: link:../desktop-preferences '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -15909,7 +19176,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15917,14 +19184,14 @@ importers: ../../plugins/desktop-preferences-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/desktop-preferences': specifier: workspace:^0.7.0 version: link:../desktop-preferences '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -15939,8 +19206,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -16002,21 +19269,21 @@ importers: ../../plugins/devmodel: dependencies: '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -16049,7 +19316,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16063,23 +19330,23 @@ importers: specifier: workspace:^0.7.0 version: link:../chunter '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/devmodel': specifier: workspace:^0.7.0 version: link:../devmodel '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -16103,8 +19370,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -16166,18 +19433,18 @@ importers: ../../plugins/diffview: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -16210,7 +19477,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16221,17 +19488,17 @@ importers: specifier: workspace:^0.7.0 version: link:../diffview '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -16262,7 +19529,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16270,8 +19537,8 @@ importers: ../../plugins/diffview-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/diffview': specifier: workspace:^0.7.0 version: link:../diffview @@ -16279,8 +19546,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/highlight '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -16304,8 +19571,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -16373,14 +19640,14 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -16392,8 +19659,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -16426,7 +19693,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16437,17 +19704,17 @@ importers: specifier: workspace:^0.7.0 version: link:../document '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -16478,7 +19745,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16492,8 +19759,8 @@ importers: specifier: workspace:^0.7.0 version: link:../activity-resources '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -16507,8 +19774,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/document': specifier: workspace:^0.7.0 version: link:../document @@ -16522,8 +19789,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -16531,8 +19798,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting @@ -16574,8 +19841,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -16637,11 +19904,11 @@ importers: ../../plugins/drive: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -16650,8 +19917,8 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -16684,7 +19951,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16695,17 +19962,17 @@ importers: specifier: workspace:^0.7.0 version: link:../drive '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -16736,7 +20003,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16744,14 +20011,14 @@ importers: ../../plugins/drive-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/contact-resources': specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../drive @@ -16759,8 +20026,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -16784,8 +20051,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -16847,11 +20114,11 @@ importers: ../../plugins/emoji: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -16866,8 +20133,8 @@ importers: version: 16.0.0 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -16900,7 +20167,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16911,17 +20178,17 @@ importers: specifier: workspace:^0.7.0 version: link:../emoji '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -16952,7 +20219,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16960,14 +20227,14 @@ importers: ../../plugins/emoji-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/emoji': specifier: workspace:^0.7.0 version: link:../emoji '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -16988,13 +20255,13 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -17054,18 +20321,18 @@ importers: ../../plugins/export: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -17101,7 +20368,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17112,17 +20379,17 @@ importers: specifier: workspace:^0.7.0 version: link:../export '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -17156,7 +20423,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17164,14 +20431,14 @@ importers: ../../plugins/export-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/export': specifier: workspace:^0.7.0 version: link:../export '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -17183,8 +20450,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -17246,18 +20513,18 @@ importers: ../../plugins/global-profile: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -17290,7 +20557,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17301,17 +20568,17 @@ importers: specifier: workspace:^0.7.0 version: link:../global-profile '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -17342,7 +20609,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17350,14 +20617,14 @@ importers: ../../plugins/global-profile-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/global-profile': specifier: workspace:^0.7.0 version: link:../global-profile @@ -17365,8 +20632,8 @@ importers: specifier: workspace:^0.7.0 version: link:../login '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -17384,8 +20651,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -17450,14 +20717,14 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting @@ -17466,8 +20733,8 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -17500,7 +20767,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17511,17 +20778,17 @@ importers: specifier: workspace:^0.7.0 version: link:../gmail '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -17552,7 +20819,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17560,11 +20827,11 @@ importers: ../../plugins/gmail-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -17587,8 +20854,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/gmail': specifier: workspace:^0.7.0 version: link:../gmail @@ -17605,8 +20872,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -17620,8 +20887,8 @@ importers: specifier: workspace:^0.7.0 version: link:../templates '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../text-editor @@ -17629,8 +20896,8 @@ importers: specifier: workspace:^0.7.0 version: link:../text-editor-resources '@hcengineering/text-html': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text-html '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -17639,8 +20906,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -17702,18 +20969,18 @@ importers: ../../plugins/guest: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -17746,7 +21013,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17757,17 +21024,17 @@ importers: specifier: workspace:^0.7.0 version: link:../guest '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -17798,7 +21065,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17806,20 +21073,20 @@ importers: ../../plugins/guest-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/guest': specifier: workspace:^0.7.0 version: link:../guest @@ -17827,8 +21094,8 @@ importers: specifier: workspace:^0.7.0 version: link:../login '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -17855,8 +21122,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -17921,21 +21188,21 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/view': specifier: workspace:^0.7.0 version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -17968,7 +21235,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17979,17 +21246,17 @@ importers: specifier: workspace:^0.7.0 version: link:../hr '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -18020,7 +21287,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18028,8 +21295,8 @@ importers: ../../plugins/hr-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -18049,8 +21316,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/hr': specifier: workspace:^0.7.0 version: link:../hr @@ -18058,8 +21325,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -18067,8 +21334,8 @@ importers: specifier: workspace:^0.7.0 version: link:../setting '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor-resources': specifier: workspace:^0.7.0 version: link:../text-editor-resources @@ -18092,8 +21359,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -18155,11 +21422,11 @@ importers: ../../plugins/huly-mail: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting @@ -18168,8 +21435,8 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -18202,7 +21469,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18213,17 +21480,17 @@ importers: specifier: workspace:^0.7.0 version: link:../huly-mail '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -18254,7 +21521,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18262,11 +21529,11 @@ importers: ../../plugins/huly-mail-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/card': specifier: workspace:^0.7.0 version: link:../card @@ -18277,8 +21544,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/huly-mail': specifier: workspace:^0.7.0 version: link:../huly-mail @@ -18292,8 +21559,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -18314,8 +21581,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -18377,15 +21644,15 @@ importers: ../../plugins/image-cropper: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -18418,7 +21685,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18426,8 +21693,8 @@ importers: ../../plugins/image-cropper-resources: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform cropperjs: specifier: ~1.5.12 version: 1.5.13 @@ -18439,8 +21706,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -18502,15 +21769,15 @@ importers: ../../plugins/inbox: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -18543,7 +21810,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18554,17 +21821,17 @@ importers: specifier: workspace:^0.7.0 version: link:../inbox '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -18595,7 +21862,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18609,8 +21876,8 @@ importers: specifier: workspace:^0.7.0 version: link:../activity-resources '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/card': specifier: workspace:^0.7.0 version: link:../card @@ -18624,11 +21891,11 @@ importers: specifier: workspace:^0.7.0 version: link:../communication-resources '@hcengineering/communication-shared': - specifier: ^0.7.11 - version: 0.7.11 + specifier: workspace:^0.7.11 + version: link:../../foundations/communication/packages/shared '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact @@ -18636,8 +21903,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/emoji-resources': specifier: workspace:^0.7.0 version: link:../emoji-resources @@ -18651,20 +21918,20 @@ importers: specifier: workspace:^0.7.0 version: link:../notification-resources '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/text-markdown '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -18682,8 +21949,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -18745,15 +22012,15 @@ importers: ../../plugins/inventory: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -18786,7 +22053,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18797,17 +22064,17 @@ importers: specifier: workspace:^0.7.0 version: link:../inventory '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -18838,7 +22105,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18846,8 +22113,8 @@ importers: ../../plugins/inventory-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/inventory': specifier: workspace:^0.7.0 version: link:../inventory @@ -18858,8 +22125,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -18880,8 +22147,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -18946,11 +22213,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/task': specifier: workspace:^0.7.0 version: link:../task @@ -18959,8 +22226,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -18993,7 +22260,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19004,17 +22271,17 @@ importers: specifier: workspace:^0.7.0 version: link:../lead '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -19045,7 +22312,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19053,8 +22320,8 @@ importers: ../../plugins/lead-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -19071,8 +22338,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/lead': specifier: workspace:^0.7.0 version: link:../lead @@ -19089,8 +22356,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -19101,8 +22368,8 @@ importers: specifier: workspace:^0.7.0 version: link:../task-resources '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor-resources': specifier: workspace:^0.7.0 version: link:../text-editor-resources @@ -19129,8 +22396,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -19192,21 +22459,21 @@ importers: ../../plugins/login: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -19239,7 +22506,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19250,17 +22517,17 @@ importers: specifier: workspace:^0.7.0 version: link:../login '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -19291,7 +22558,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19299,23 +22566,23 @@ importers: ../../plugins/login-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-providers': specifier: workspace:^0.7.0 version: link:../../packages/analytics-providers '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../login '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -19336,8 +22603,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -19408,8 +22675,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../drive @@ -19417,8 +22684,8 @@ importers: specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -19433,8 +22700,8 @@ importers: version: link:../workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -19470,7 +22737,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19481,17 +22748,17 @@ importers: specifier: workspace:^0.7.0 version: link:../love '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -19525,7 +22792,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19533,8 +22800,8 @@ importers: ../../plugins/love-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/activity': specifier: workspace:^0.7.0 version: link:../activity @@ -19545,8 +22812,8 @@ importers: specifier: workspace:^0.7.0 version: link:../ai-bot-resources '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/calendar': specifier: workspace:^0.7.0 version: link:../calendar @@ -19563,8 +22830,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/emoji': specifier: workspace:^0.7.0 version: link:../emoji @@ -19596,8 +22863,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -19633,8 +22900,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -19699,15 +22966,15 @@ importers: specifier: workspace:^0.7.0 version: link:../card '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -19740,7 +23007,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19751,17 +23018,17 @@ importers: specifier: workspace:^0.7.0 version: link:../mail '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -19792,7 +23059,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19800,11 +23067,11 @@ importers: ../../plugins/media: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -19813,8 +23080,8 @@ importers: version: 3.3.0 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/events': specifier: ^3.0.3 version: 3.0.3 @@ -19850,7 +23117,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typed-emitter: specifier: ^2.1.0 version: 2.1.0 @@ -19864,17 +23131,17 @@ importers: specifier: workspace:^0.7.0 version: link:../media '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -19905,7 +23172,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19913,14 +23180,14 @@ importers: ../../plugins/media-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/media': specifier: workspace:^0.7.0 version: link:../media '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -19944,8 +23211,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/events': specifier: ^3.0.3 version: 3.0.3 @@ -19953,7 +23220,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -20022,11 +23289,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -20041,8 +23308,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -20078,7 +23345,7 @@ importers: version: 1.94.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20089,17 +23356,17 @@ importers: specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -20130,7 +23397,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20144,8 +23411,8 @@ importers: specifier: workspace:^0.7.0 version: link:../activity-resources '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -20162,8 +23429,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/emoji-resources': specifier: workspace:^0.7.0 version: link:../emoji-resources @@ -20171,8 +23438,8 @@ importers: specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -20180,8 +23447,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -20199,8 +23466,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -20262,21 +23529,21 @@ importers: ../../plugins/onboard: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../login '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -20309,7 +23576,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20320,17 +23587,17 @@ importers: specifier: workspace:^0.7.0 version: link:../onboard '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -20361,7 +23628,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20369,11 +23636,11 @@ importers: ../../plugins/onboard-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../login @@ -20384,8 +23651,8 @@ importers: specifier: workspace:^0.7.0 version: link:../onboard '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -20406,8 +23673,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -20472,11 +23739,11 @@ importers: ../../plugins/openai: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform js-tiktoken: specifier: ^1.0.14 version: 1.0.21 @@ -20485,13 +23752,13 @@ importers: version: 4.104.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -20525,7 +23792,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20533,18 +23800,18 @@ importers: ../../plugins/preference: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -20577,7 +23844,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20585,20 +23852,20 @@ importers: ../../plugins/preference-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -20629,7 +23896,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20640,18 +23907,18 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -20684,7 +23951,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20698,14 +23965,14 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/hulypulse-client': specifier: workspace:^0.7.0 version: link:../../packages/hulypulse-client '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presence': specifier: workspace:^0.7.0 version: link:../presence @@ -20732,8 +23999,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -20795,18 +24062,18 @@ importers: ../../plugins/print: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -20842,7 +24109,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20850,20 +24117,20 @@ importers: ../../plugins/print-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/print': specifier: workspace:^0.7.0 version: link:../print devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -20897,7 +24164,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20905,17 +24172,17 @@ importers: ../../plugins/print-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/guest': specifier: workspace:^0.7.0 version: link:../guest '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -20939,8 +24206,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -21005,11 +24272,11 @@ importers: specifier: workspace:^0.7.0 version: link:../card '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/time': specifier: workspace:^0.7.0 version: link:../time @@ -21021,8 +24288,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -21055,7 +24322,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21063,20 +24330,20 @@ importers: ../../plugins/process-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/process': specifier: workspace:^0.7.0 version: link:../process devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -21110,7 +24377,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21118,8 +24385,8 @@ importers: ../../plugins/process-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/calendar': specifier: workspace:^0.7.0 version: link:../calendar @@ -21136,11 +24403,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -21148,8 +24415,8 @@ importers: specifier: workspace:^0.7.0 version: link:../process '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/setting-resources': specifier: workspace:^0.7.0 version: link:../setting-resources @@ -21176,8 +24443,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/html-to-text': specifier: ^8.1.1 version: 8.1.1 @@ -21251,11 +24518,11 @@ importers: specifier: workspace:^0.7.0 version: link:../controlled-documents '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -21264,8 +24531,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -21301,7 +24568,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21309,20 +24576,20 @@ importers: ../../plugins/products-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/products': specifier: workspace:^0.7.0 version: link:../products devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -21356,7 +24623,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21379,8 +24646,8 @@ importers: specifier: workspace:^0.7.0 version: link:../controlled-documents '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification @@ -21388,8 +24655,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -21425,8 +24692,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -21494,18 +24761,18 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/theme': specifier: workspace:^0.7.0 version: link:../../packages/theme devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -21538,7 +24805,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21546,20 +24813,20 @@ importers: ../../plugins/questions-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/questions': specifier: workspace:^0.7.0 version: link:../questions devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -21590,7 +24857,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21601,11 +24868,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -21638,8 +24905,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -21701,18 +24968,18 @@ importers: ../../plugins/rating: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -21745,7 +25012,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21753,20 +25020,20 @@ importers: ../../plugins/rating-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rating': specifier: workspace:^0.7.0 version: link:../rating devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -21797,7 +25064,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21805,8 +25072,8 @@ importers: ../../plugins/rating-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact @@ -21814,8 +25081,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/emoji': specifier: workspace:^0.7.0 version: link:../emoji @@ -21823,8 +25090,8 @@ importers: specifier: workspace:^0.7.0 version: link:../emoji-resources '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -21845,8 +25112,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -21908,14 +25175,14 @@ importers: ../../plugins/recorder: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../drive '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/uploader': specifier: workspace:^0.7.0 version: link:../uploader @@ -21924,8 +25191,8 @@ importers: version: 4.3.1 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -21958,7 +25225,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21966,20 +25233,20 @@ importers: ../../plugins/recorder-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/recorder': specifier: workspace:^0.7.0 version: link:../recorder devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -22010,7 +25277,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22018,8 +25285,8 @@ importers: ../../plugins/recorder-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact @@ -22027,8 +25294,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../drive @@ -22039,8 +25306,8 @@ importers: specifier: workspace:^0.7.0 version: link:../media-resources '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -22073,13 +25340,13 @@ importers: version: 4.3.1 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -22148,11 +25415,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/survey': specifier: workspace:^0.7.0 version: link:../survey @@ -22170,8 +25437,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -22204,7 +25471,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22212,20 +25479,20 @@ importers: ../../plugins/recruit-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/recruit': specifier: workspace:^0.7.0 version: link:../recruit devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -22256,7 +25523,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22267,8 +25534,8 @@ importers: specifier: workspace:^0.7.0 version: link:../activity '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -22291,8 +25558,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/export-resources': specifier: workspace:^0.7.0 version: link:../export-resources @@ -22309,8 +25576,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -22339,8 +25606,8 @@ importers: specifier: workspace:^0.7.0 version: link:../task-resources '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor-resources': specifier: workspace:^0.7.0 version: link:../text-editor-resources @@ -22364,8 +25631,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -22433,18 +25700,18 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -22477,7 +25744,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22485,20 +25752,20 @@ importers: ../../plugins/request-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/request': specifier: workspace:^0.7.0 version: link:../request devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -22529,7 +25796,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22558,11 +25825,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -22570,8 +25837,8 @@ importers: specifier: workspace:^0.7.0 version: link:../request '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../text-editor @@ -22589,8 +25856,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -22652,14 +25919,14 @@ importers: ../../plugins/setting: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/templates': specifier: workspace:^0.7.0 version: link:../templates @@ -22668,8 +25935,8 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -22702,7 +25969,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22710,20 +25977,20 @@ importers: ../../plugins/setting-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -22754,7 +26021,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22762,11 +26029,11 @@ importers: ../../plugins/setting-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -22786,8 +26053,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/integration-client': specifier: workspace:^0.7.0 version: link:../../packages/integration-client @@ -22795,20 +26062,20 @@ importers: specifier: workspace:^0.7.0 version: link:../login '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/panel': specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/rating': specifier: workspace:^0.7.0 version: link:../rating @@ -22844,8 +26111,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -22907,12 +26174,12 @@ importers: ../../plugins/sign: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -22948,7 +26215,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22956,15 +26223,15 @@ importers: ../../plugins/support: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -22997,7 +26264,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23005,20 +26272,20 @@ importers: ../../plugins/support-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/support': specifier: workspace:^0.7.0 version: link:../support devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -23049,7 +26316,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23057,11 +26324,11 @@ importers: ../../plugins/support-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -23076,8 +26343,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -23139,11 +26406,11 @@ importers: ../../plugins/survey: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -23152,8 +26419,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -23186,7 +26453,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23194,20 +26461,20 @@ importers: ../../plugins/survey-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/survey': specifier: workspace:^0.7.0 version: link:../survey devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -23238,7 +26505,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23246,17 +26513,17 @@ importers: ../../plugins/survey-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/panel': specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -23280,8 +26547,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -23343,11 +26610,11 @@ importers: ../../plugins/tags: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -23356,8 +26623,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -23390,7 +26657,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23398,20 +26665,20 @@ importers: ../../plugins/tags-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/tags': specifier: workspace:^0.7.0 version: link:../tags devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -23442,7 +26709,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23450,14 +26717,14 @@ importers: ../../plugins/tags-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -23478,8 +26745,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -23544,17 +26811,17 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui @@ -23563,8 +26830,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -23597,7 +26864,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23605,20 +26872,20 @@ importers: ../../plugins/task-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/task': specifier: workspace:^0.7.0 version: link:../task devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -23649,7 +26916,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23660,8 +26927,8 @@ importers: specifier: workspace:^0.7.0 version: link:../activity '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -23681,8 +26948,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/emoji': specifier: workspace:^0.7.0 version: link:../emoji @@ -23702,8 +26969,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -23742,8 +27009,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -23811,14 +27078,14 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting @@ -23830,8 +27097,8 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -23864,7 +27131,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23872,20 +27139,20 @@ importers: ../../plugins/telegram-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/telegram': specifier: workspace:^0.7.0 version: link:../telegram devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -23916,7 +27183,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23924,11 +27191,11 @@ importers: ../../plugins/telegram-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -23948,8 +27215,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/integration-client': specifier: workspace:^0.7.0 version: link:../../packages/integration-client @@ -23966,14 +27233,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/retry': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/retry '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting @@ -23987,8 +27254,8 @@ importers: specifier: workspace:^0.7.0 version: link:../templates '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor-resources': specifier: workspace:^0.7.0 version: link:../text-editor-resources @@ -24006,8 +27273,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -24069,18 +27336,18 @@ importers: ../../plugins/templates: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -24113,7 +27380,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24121,20 +27388,20 @@ importers: ../../plugins/templates-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/templates': specifier: workspace:^0.7.0 version: link:../templates devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -24165,7 +27432,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24176,11 +27443,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -24207,8 +27474,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -24279,11 +27546,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -24304,8 +27571,8 @@ importers: version: 1.0.5 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -24338,7 +27605,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24346,20 +27613,20 @@ importers: ../../plugins/test-management-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/test-management': specifier: workspace:^0.7.0 version: link:../test-management devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -24390,7 +27657,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24404,8 +27671,8 @@ importers: specifier: workspace:^0.7.0 version: link:../activity-resources '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -24422,8 +27689,8 @@ importers: specifier: workspace:^0.7.0 version: link:../chunter-resources '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact @@ -24431,8 +27698,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/kanban': specifier: workspace:^0.7.0 version: link:../../packages/kanban @@ -24449,8 +27716,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -24458,8 +27725,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/query': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/query '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting @@ -24476,8 +27743,8 @@ importers: specifier: workspace:^0.7.0 version: link:../test-management '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor-resources': specifier: workspace:^0.7.0 version: link:../text-editor-resources @@ -24504,8 +27771,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -24567,11 +27834,11 @@ importers: ../../plugins/text-editor: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -24586,8 +27853,8 @@ importers: version: 2.27.1 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/diff': specifier: ~5.0.2 version: 5.0.9 @@ -24626,7 +27893,7 @@ importers: version: 1.94.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24634,20 +27901,20 @@ importers: ../../plugins/text-editor-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../text-editor devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -24678,7 +27945,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24686,20 +27953,20 @@ importers: ../../plugins/text-editor-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/chunter': specifier: workspace:^0.7.0 version: link:../chunter '@hcengineering/collaborator-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/collaborator-client '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../drive @@ -24710,8 +27977,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/highlight '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presence': specifier: workspace:^0.7.0 version: link:../presence @@ -24719,20 +27986,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../text-editor '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/text-markdown '@hcengineering/text-ydoc': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text-ydoc '@hcengineering/theme': specifier: workspace:^0.7.0 version: link:../../packages/theme @@ -24873,8 +28140,8 @@ importers: version: 13.6.27 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/diff': specifier: ~5.0.2 version: 5.0.9 @@ -24945,14 +28212,14 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/task': specifier: workspace:^0.7.0 version: link:../task @@ -24961,8 +28228,8 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -24995,7 +28262,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25003,20 +28270,20 @@ importers: ../../plugins/time-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/time': specifier: workspace:^0.7.0 version: link:../time devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -25047,7 +28314,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25061,8 +28328,8 @@ importers: specifier: workspace:^0.7.0 version: link:../activity-resources '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/board': specifier: workspace:^0.7.0 version: link:../board @@ -25079,8 +28346,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/document': specifier: workspace:^0.7.0 version: link:../document @@ -25088,14 +28355,14 @@ importers: specifier: workspace:^0.7.0 version: link:../lead '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/recruit': specifier: workspace:^0.7.0 version: link:../recruit @@ -25146,8 +28413,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -25218,11 +28485,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -25246,8 +28513,8 @@ importers: version: 1.0.5 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -25280,7 +28547,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25288,20 +28555,20 @@ importers: ../../plugins/tracker-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/tracker': specifier: workspace:^0.7.0 version: link:../tracker devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -25332,7 +28599,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25346,8 +28613,8 @@ importers: specifier: workspace:^0.7.0 version: link:../activity-resources '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../attachment @@ -25364,8 +28631,8 @@ importers: specifier: workspace:^0.7.0 version: link:../chunter-resources '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact @@ -25373,8 +28640,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/kanban': specifier: workspace:^0.7.0 version: link:../../packages/kanban @@ -25391,8 +28658,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -25400,8 +28667,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/query': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/query '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting @@ -25418,8 +28685,8 @@ importers: specifier: workspace:^0.7.0 version: link:../task-resources '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor-resources': specifier: workspace:^0.7.0 version: link:../text-editor-resources @@ -25449,8 +28716,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -25518,11 +28785,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/questions': specifier: workspace:^0.7.0 version: link:../questions @@ -25531,8 +28798,8 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -25565,7 +28832,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25573,20 +28840,20 @@ importers: ../../plugins/training-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/training': specifier: workspace:^0.7.0 version: link:../training devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -25617,7 +28884,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25646,8 +28913,8 @@ importers: specifier: workspace:^0.7.0 version: link:../controlled-documents '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification @@ -25655,8 +28922,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -25698,8 +28965,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -25761,18 +29028,18 @@ importers: ../../plugins/uploader: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -25805,7 +29072,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25813,20 +29080,20 @@ importers: ../../plugins/uploader-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/uploader': specifier: workspace:^0.7.0 version: link:../uploader devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -25857,7 +29124,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25865,11 +29132,11 @@ importers: ../../plugins/uploader-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation @@ -25899,13 +29166,13 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -25965,11 +29232,11 @@ importers: ../../plugins/view: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -25978,8 +29245,8 @@ importers: version: link:../../packages/ui devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -26012,7 +29279,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26020,23 +29287,23 @@ importers: ../../plugins/view-assets: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/view': specifier: workspace:^0.7.0 version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -26067,7 +29334,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26075,8 +29342,8 @@ importers: ../../plugins/view-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/card': specifier: workspace:^0.7.0 version: link:../card @@ -26084,8 +29351,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/emoji': specifier: workspace:^0.7.0 version: link:../emoji @@ -26096,8 +29363,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/hls '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification @@ -26105,8 +29372,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -26114,8 +29381,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/presentation '@hcengineering/query': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/query '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting @@ -26123,8 +29390,8 @@ importers: specifier: workspace:^0.7.0 version: link:../task '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../text-editor @@ -26132,8 +29399,8 @@ importers: specifier: workspace:^0.7.0 version: link:../text-editor-resources '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/text-markdown '@hcengineering/theme': specifier: workspace:^0.7.0 version: link:../../packages/theme @@ -26154,8 +29421,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -26217,14 +29484,14 @@ importers: ../../plugins/workbench: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -26236,8 +29503,8 @@ importers: version: link:../view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -26270,7 +29537,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26278,20 +29545,20 @@ importers: ../../plugins/workbench-assets: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/workbench': specifier: workspace:^0.7.0 version: link:../workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -26322,7 +29589,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26330,11 +29597,11 @@ importers: ../../plugins/workbench-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/calendar': specifier: workspace:^0.7.0 version: link:../calendar @@ -26342,14 +29609,14 @@ importers: specifier: workspace:^0.7.0 version: link:../chat '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/communication': specifier: workspace:^0.7.0 version: link:../communication '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact @@ -26357,8 +29624,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/inbox': specifier: workspace:^0.7.0 version: link:../inbox @@ -26372,8 +29639,8 @@ importers: specifier: workspace:^0.7.0 version: link:../notification-resources '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../preference @@ -26418,8 +29685,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -26487,26 +29754,26 @@ importers: specifier: workspace:^0.7.0 version: link:../../server/account-service '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/auth-providers': specifier: workspace:^0.7.0 version: link:../authProviders '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@koa/cors': specifier: ^5.0.0 version: 5.0.0 @@ -26524,8 +29791,8 @@ importers: version: 6.21.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -26542,7 +29809,7 @@ importers: specifier: ^5.0.0 version: 5.0.1 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -26554,7 +29821,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -26581,7 +29848,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -26593,11 +29860,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../server/account '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core koa: specifier: ^2.15.4 version: 2.16.3 @@ -26630,8 +29897,8 @@ importers: version: 2.2.0 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -26657,7 +29924,7 @@ importers: specifier: ^2.0.2 version: 2.0.2 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -26688,7 +29955,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26696,41 +29963,41 @@ importers: ../../pods/backup: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/backup-service': specifier: workspace:^0.7.0 version: link:../../server/backup-service '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/mongo '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/postgres': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/server/packages/postgres '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-pipeline': specifier: workspace:^0.7.0 version: link:../../server/server-pipeline '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token dotenv: specifier: ^16.4.5 version: 16.6.1 @@ -26739,13 +30006,13 @@ importers: specifier: workspace:^0.7.0 version: link:../../models/all '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -26757,7 +30024,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -26784,7 +30051,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -26793,32 +30060,32 @@ importers: ../../pods/collaborator: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/collaborator': specifier: workspace:^0.7.0 version: link:../../server/collaborator '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -26830,7 +30097,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -26857,7 +30124,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -26890,32 +30157,32 @@ importers: ../../pods/front: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../plugins/attachment '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/front': specifier: workspace:^0.7.0 version: link:../../server/front '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/prod': specifier: workspace:^1.0.1 version: link:../../dev/prod '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token body-parser: specifier: ^1.20.3 version: 1.20.3 @@ -26936,8 +30203,8 @@ importers: version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/body-parser': specifier: ~1.19.2 version: 1.19.6 @@ -26957,7 +30224,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 @@ -26972,7 +30239,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -26999,7 +30266,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -27008,56 +30275,56 @@ importers: ../../pods/fulltext: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-server': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/server '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/elastic': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/elastic '@hcengineering/hulylake-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/hulylake-client '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/server/packages/kafka '@hcengineering/middleware': - specifier: ^0.7.21 - version: 0.7.21 + specifier: workspace:^0.7.21 + version: link:../../foundations/server/packages/middleware '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/mongo '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/postgres': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/server/packages/postgres '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-collaboration': specifier: workspace:^0.7.0 version: link:../../server-plugins/collaboration '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-indexer': specifier: workspace:^0.7.0 version: link:../../server/indexer @@ -27065,11 +30332,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../server/server-pipeline '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@koa/cors': specifier: ^5.0.0 version: 5.0.0 @@ -27090,8 +30357,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../models/all '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -27108,7 +30375,7 @@ importers: specifier: ^5.0.0 version: 5.0.1 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -27120,7 +30387,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -27147,7 +30414,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -27156,50 +30423,50 @@ importers: ../../pods/media: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/api-client': - specifier: ^0.7.18 - version: 0.7.18(bufferutil@4.0.9)(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/api-client '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../plugins/attachment '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../../plugins/drive '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/server/packages/kafka '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token dotenv: specifier: ^16.4.5 version: 16.6.1 @@ -27208,13 +30475,13 @@ importers: version: 2.2.4 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -27226,7 +30493,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -27256,7 +30523,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -27265,29 +30532,29 @@ importers: ../../pods/preview: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token blurhash: specifier: ^2.0.5 version: 2.0.5 @@ -27320,8 +30587,8 @@ importers: version: 0.34.5 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/cors': specifier: ^2.8.12 version: 2.8.19 @@ -27338,7 +30605,7 @@ importers: specifier: ~1.9.9 version: 1.9.10 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -27350,7 +30617,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -27380,7 +30647,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -27389,44 +30656,44 @@ importers: ../../pods/server: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-server': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/server '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/server/packages/kafka '@hcengineering/middleware': - specifier: ^0.7.21 - version: 0.7.21 + specifier: workspace:^0.7.21 + version: link:../../foundations/server/packages/middleware '@hcengineering/minio': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/minio '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/mongo '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/pod-telegram-bot': specifier: workspace:^0.7.0 version: link:../../services/telegram-bot/pod-telegram-bot @@ -27434,14 +30701,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../services/translate '@hcengineering/postgres': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/server/packages/postgres '@hcengineering/rpc': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rpc '@hcengineering/server': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/server '@hcengineering/server-ai-bot': specifier: workspace:^0.7.0 version: link:../../server-plugins/ai-bot @@ -27452,8 +30719,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../server-plugins/card '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification @@ -27461,14 +30728,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../server/server-pipeline '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-telegram': specifier: workspace:^0.7.0 version: link:../../server-plugins/telegram '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token body-parser: specifier: ^1.20.3 version: 1.20.3 @@ -27504,8 +30771,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../models/all '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/body-parser': specifier: ~1.19.2 version: 1.19.6 @@ -27522,10 +30789,10 @@ importers: specifier: ~1.9.9 version: 1.9.10 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -27537,7 +30804,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -27570,7 +30837,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -27579,23 +30846,23 @@ importers: ../../pods/stats: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@koa/cors': specifier: ^5.0.0 version: 5.0.0 @@ -27610,8 +30877,8 @@ importers: version: 12.0.1 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -27628,7 +30895,7 @@ importers: specifier: ^5.0.0 version: 5.0.1 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -27640,7 +30907,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -27667,7 +30934,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -27676,29 +30943,29 @@ importers: ../../pods/workspace: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/server/packages/kafka '@hcengineering/model-all': specifier: workspace:^0.7.0 version: link:../../models/all '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/workspace-service': specifier: workspace:^0.7.0 version: link:../../server/workspace-service @@ -27707,13 +30974,13 @@ importers: version: 6.21.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -27725,7 +30992,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -27752,7 +31019,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -27774,10 +31041,10 @@ importers: specifier: workspace:^0.7.0 version: link:../desktop '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../foundations/utils/packages/platform-rig '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -27814,7 +31081,7 @@ importers: version: 6.6.0(eslint@8.57.1) node-loader: specifier: ~2.0.0 - version: 2.0.0(webpack@5.103.0(@swc/core@1.15.3)) + version: 2.0.0(webpack@5.103.0) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -27822,11 +31089,11 @@ importers: ../../qms-tests/sanity: dependencies: '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core cross-env: specifier: ~7.0.3 version: 7.0.3 @@ -27838,8 +31105,8 @@ importers: specifier: ^8.4.1 version: 8.4.1 '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@playwright/test': specifier: ^1.48.2 version: 1.56.1 @@ -27847,7 +31114,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -27886,26 +31153,26 @@ importers: ../../server-plugins/activity: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -27936,7 +31203,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -27950,23 +31217,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/card '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-activity': specifier: workspace:^0.7.0 version: link:../activity @@ -27977,23 +31244,23 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification-resources': specifier: workspace:^0.7.0 version: link:../notification-resources '@hcengineering/text-core': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text-core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -28024,7 +31291,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28035,23 +31302,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/ai-bot '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -28082,7 +31349,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28105,14 +31372,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-activity-resources': specifier: workspace:^0.7.0 version: link:../activity-resources @@ -28123,21 +31390,21 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-templates': specifier: workspace:^0.7.0 version: link:../templates '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/templates': specifier: workspace:^0.7.0 version: link:../../plugins/templates devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -28170,7 +31437,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28181,23 +31448,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/analytics-collector '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -28228,7 +31495,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28245,14 +31512,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-templates': specifier: workspace:^0.7.0 version: link:../templates @@ -28261,8 +31528,8 @@ importers: version: link:../../plugins/templates devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -28295,7 +31562,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28303,23 +31570,23 @@ importers: ../../server-plugins/attachment: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -28350,7 +31617,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28361,18 +31628,18 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/attachment '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -28405,7 +31672,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28413,26 +31680,26 @@ importers: ../../server-plugins/calendar: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -28463,7 +31730,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28477,14 +31744,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/server/packages/kafka '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-calendar': specifier: workspace:^0.7.0 version: link:../calendar @@ -28492,18 +31759,18 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification-resources': specifier: workspace:^0.7.0 version: link:../notification-resources '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -28536,7 +31803,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28544,23 +31811,23 @@ importers: ../../server-plugins/card: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -28591,7 +31858,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28605,26 +31872,26 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/communication '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -28633,8 +31900,8 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -28667,7 +31934,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28675,26 +31942,26 @@ importers: ../../server-plugins/chunter: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -28725,7 +31992,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28742,8 +32009,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../../plugins/login @@ -28751,17 +32018,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/server '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -28769,11 +32036,11 @@ importers: specifier: workspace:^0.7.0 version: link:../notification-resources '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-core': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text-core '@hcengineering/view': specifier: workspace:^0.7.0 version: link:../../plugins/view @@ -28782,8 +32049,8 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -28816,7 +32083,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28824,26 +32091,26 @@ importers: ../../server-plugins/collaboration: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-indexer': specifier: workspace:^0.7.0 version: link:../../server/indexer devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -28874,7 +32141,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28882,18 +32149,18 @@ importers: ../../server-plugins/collaboration-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -28926,7 +32193,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28937,14 +32204,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -28953,13 +32220,13 @@ importers: version: link:../templates devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -28990,7 +32257,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29004,8 +32271,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../../plugins/login @@ -29013,17 +32280,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/view': specifier: workspace:^0.7.0 version: link:../../plugins/view @@ -29032,8 +32299,8 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -29066,7 +32333,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29074,23 +32341,23 @@ importers: ../../server-plugins/controlled-documents: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -29124,7 +32391,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29141,14 +32408,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/controlled-documents '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/request': specifier: workspace:^0.7.0 version: link:../../plugins/request @@ -29159,11 +32426,11 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/training': specifier: workspace:^0.7.0 version: link:../../plugins/training @@ -29175,13 +32442,13 @@ importers: version: 1.6.6 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -29215,7 +32482,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29223,26 +32490,26 @@ importers: ../../server-plugins/document: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -29276,7 +32543,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29284,17 +32551,17 @@ importers: ../../server-plugins/document-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/document': specifier: workspace:^0.7.0 version: link:../../plugins/document '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/workbench': specifier: workspace:^0.7.0 version: link:../../plugins/workbench @@ -29303,13 +32570,13 @@ importers: version: 1.6.6 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -29343,7 +32610,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29351,23 +32618,23 @@ importers: ../../server-plugins/drive: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -29398,7 +32665,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29406,21 +32673,21 @@ importers: ../../server-plugins/drive-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../../plugins/drive '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -29453,7 +32720,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29461,26 +32728,26 @@ importers: ../../server-plugins/gmail: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -29511,7 +32778,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29528,8 +32795,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/gmail': specifier: workspace:^0.7.0 version: link:../../plugins/gmail @@ -29537,14 +32804,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -29553,8 +32820,8 @@ importers: version: link:../notification-resources devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -29587,7 +32854,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29595,23 +32862,23 @@ importers: ../../server-plugins/guest: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -29642,7 +32909,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29650,27 +32917,27 @@ importers: ../../server-plugins/guest-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/guest': specifier: workspace:^0.7.0 version: link:../../plugins/guest '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/view': specifier: workspace:^0.7.0 version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -29703,7 +32970,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29711,26 +32978,26 @@ importers: ../../server-plugins/hr: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -29761,7 +33028,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29772,8 +33039,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/gmail': specifier: workspace:^0.7.0 version: link:../../plugins/gmail @@ -29784,14 +33051,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-gmail-resources': specifier: workspace:^0.7.0 version: link:../gmail-resources @@ -29803,8 +33070,8 @@ importers: version: link:../notification-resources devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -29837,7 +33104,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29845,20 +33112,20 @@ importers: ../../server-plugins/inventory: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -29889,7 +33156,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29897,8 +33164,8 @@ importers: ../../server-plugins/inventory-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/inventory': specifier: workspace:^0.7.0 version: link:../../plugins/inventory @@ -29906,11 +33173,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/login '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -29922,8 +33189,8 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -29956,7 +33223,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29964,23 +33231,23 @@ importers: ../../server-plugins/lead: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -30011,7 +33278,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30022,8 +33289,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/lead': specifier: workspace:^0.7.0 version: link:../../plugins/lead @@ -30031,11 +33298,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/login '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-task-resources': specifier: workspace:^0.7.0 version: link:../task-resources @@ -30047,8 +33314,8 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -30081,7 +33348,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30089,26 +33356,26 @@ importers: ../../server-plugins/love: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -30142,7 +33409,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30153,8 +33420,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/love': specifier: workspace:^0.7.0 version: link:../../plugins/love @@ -30162,14 +33429,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification-resources': specifier: workspace:^0.7.0 version: link:../notification-resources @@ -30181,8 +33448,8 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -30218,7 +33485,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30232,26 +33499,26 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -30282,7 +33549,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30293,8 +33560,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/activity '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/chunter': specifier: workspace:^0.7.0 version: link:../../plugins/chunter @@ -30302,20 +33569,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -30323,8 +33590,8 @@ importers: specifier: workspace:^0.7.0 version: link:../view '@hcengineering/text-core': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text-core '@hcengineering/view': specifier: workspace:^0.7.0 version: link:../../plugins/view @@ -30333,8 +33600,8 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -30367,7 +33634,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30375,26 +33642,26 @@ importers: ../../server-plugins/preference: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../plugins/preference '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -30425,7 +33692,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30436,29 +33703,29 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/card '@hcengineering/collaborator-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/collaborator-client '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/process': specifier: workspace:^0.7.0 version: link:../../plugins/process '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -30492,7 +33759,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30506,17 +33773,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/process': specifier: workspace:^0.7.0 version: link:../../plugins/process '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-process': specifier: workspace:^0.7.0 version: link:../process @@ -30525,8 +33792,8 @@ importers: version: link:../../plugins/time devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -30562,7 +33829,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30570,26 +33837,26 @@ importers: ../../server-plugins/rating: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rating': specifier: workspace:^0.7.0 version: link:../../plugins/rating '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -30620,7 +33887,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30628,26 +33895,26 @@ importers: ../../server-plugins/recruit: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -30678,7 +33945,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30689,20 +33956,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../../plugins/login '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/recruit': specifier: workspace:^0.7.0 version: link:../../plugins/recruit '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-task-resources': specifier: workspace:^0.7.0 version: link:../task-resources @@ -30714,8 +33981,8 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -30748,7 +34015,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30756,23 +34023,23 @@ importers: ../../server-plugins/request: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -30803,7 +34070,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30817,14 +34084,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/request': specifier: workspace:^0.7.0 version: link:../../plugins/request @@ -30835,8 +34102,8 @@ importers: specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification-resources': specifier: workspace:^0.7.0 version: link:../notification-resources @@ -30848,8 +34115,8 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -30882,7 +34149,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30890,14 +34157,14 @@ importers: ../../server-plugins/setting: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -30906,13 +34173,13 @@ importers: version: link:../templates devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -30943,7 +34210,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30954,24 +34221,24 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -31004,7 +34271,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31012,23 +34279,23 @@ importers: ../../server-plugins/tags: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -31059,7 +34326,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31067,21 +34334,21 @@ importers: ../../server-plugins/tags-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/tags': specifier: workspace:^0.7.0 version: link:../../plugins/tags devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -31114,7 +34381,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31122,26 +34389,26 @@ importers: ../../server-plugins/task: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -31172,7 +34439,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31183,8 +34450,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../../plugins/login @@ -31192,11 +34459,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -31214,8 +34481,8 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -31248,7 +34515,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31259,17 +34526,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/activity '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -31278,13 +34545,13 @@ importers: version: link:../templates devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -31315,7 +34582,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31332,20 +34599,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -31356,8 +34623,8 @@ importers: specifier: workspace:^0.7.0 version: link:../telegram '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../plugins/setting @@ -31365,12 +34632,12 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/telegram '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -31403,7 +34670,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31411,26 +34678,26 @@ importers: ../../server-plugins/templates: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/templates': specifier: workspace:^0.7.0 version: link:../../plugins/templates devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -31461,7 +34728,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31469,14 +34736,14 @@ importers: ../../server-plugins/time: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/task': specifier: workspace:^0.7.0 version: link:../../plugins/task @@ -31485,13 +34752,13 @@ importers: version: link:../../plugins/time devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -31522,7 +34789,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31530,26 +34797,26 @@ importers: ../../server-plugins/time-resources: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -31563,8 +34830,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/task '@hcengineering/text-core': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text-core '@hcengineering/time': specifier: workspace:^0.7.0 version: link:../../plugins/time @@ -31573,8 +34840,8 @@ importers: version: link:../../plugins/tracker devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -31607,7 +34874,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31615,26 +34882,26 @@ importers: ../../server-plugins/tracker: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -31665,7 +34932,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31679,8 +34946,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../../plugins/login @@ -31688,14 +34955,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-contact': specifier: workspace:^0.7.0 version: link:../contact '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -31706,8 +34973,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/task '@hcengineering/text-core': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text-core '@hcengineering/tracker': specifier: workspace:^0.7.0 version: link:../../plugins/tracker @@ -31719,8 +34986,8 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -31753,7 +35020,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31761,11 +35028,11 @@ importers: ../../server-plugins/training: dependencies: '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -31774,8 +35041,8 @@ importers: version: link:../../plugins/training devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -31811,7 +35078,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31822,17 +35089,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification @@ -31844,8 +35111,8 @@ importers: version: link:../../plugins/workbench devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -31881,7 +35148,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31889,26 +35156,26 @@ importers: ../../server-plugins/view: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../notification devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -31939,7 +35206,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31947,14 +35214,14 @@ importers: ../../server-plugins/view-resources: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-view': specifier: workspace:^0.7.0 version: link:../view @@ -31963,8 +35230,8 @@ importers: version: link:../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -31997,7 +35264,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32005,32 +35272,32 @@ importers: ../../server/account: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/mongo '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/postgres': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/server/packages/postgres '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-pipeline': specifier: workspace:^0.7.0 version: link:../server-pipeline '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token mongodb: specifier: ^6.16.0 version: 6.21.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) @@ -32042,13 +35309,13 @@ importers: version: 3.4.7 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/otp-generator': specifier: ^4.0.2 @@ -32085,7 +35352,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32096,8 +35363,8 @@ importers: specifier: workspace:^0.7.0 version: link:../account '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/auth-providers': specifier: workspace:^0.7.0 version: link:../../pods/authProviders @@ -32105,20 +35372,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/mongo '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@koa/cors': specifier: ^5.0.0 version: 5.0.0 @@ -32139,8 +35406,8 @@ importers: version: 6.21.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/cookies': specifier: ^0.9.0 version: 0.9.2 @@ -32160,7 +35427,7 @@ importers: specifier: ^5.0.0 version: 5.0.1 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -32172,7 +35439,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -32199,7 +35466,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -32211,38 +35478,38 @@ importers: specifier: workspace:^0.7.0 version: link:../account '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/model-contact': specifier: workspace:^0.7.0 version: link:../../models/contact '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/server-tool': specifier: workspace:^0.7.0 version: link:../tool @@ -32254,13 +35521,13 @@ importers: version: 3.1.7 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/tar-stream': specifier: ^3.1.3 @@ -32294,7 +35561,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32302,41 +35569,41 @@ importers: ../../server/backup-service: dependencies: '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/minio': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/minio '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-backup': specifier: workspace:^0.7.0 version: link:../backup '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/server-tool': specifier: workspace:^0.7.0 version: link:../tool @@ -32345,13 +35612,13 @@ importers: version: 3.1.7 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/tar-stream': specifier: ^3.1.3 @@ -32385,7 +35652,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32396,62 +35663,62 @@ importers: specifier: workspace:^0.7.0 version: link:../account '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/activity': specifier: workspace:^0.7.0 version: link:../../plugins/activity '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/collaboration': - specifier: ^0.7.16 - version: 0.7.16(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/collaboration '@hcengineering/collaborator-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/collaborator-client '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/minio': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/minio '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/mongo '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-ydoc': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text-ydoc '@hocuspocus/server': specifier: ^2.15.2 version: 2.15.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) @@ -32484,8 +35751,8 @@ importers: version: 13.6.27 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/body-parser': specifier: ~1.19.2 version: 1.19.6 @@ -32502,10 +35769,10 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -32517,7 +35784,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -32544,7 +35811,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -32553,38 +35820,38 @@ importers: ../../server/front: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../plugins/attachment '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/minio': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/minio '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/mongo '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/storage': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/storage body-parser: specifier: ^1.20.3 version: 1.20.3 @@ -32611,8 +35878,8 @@ importers: version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/body-parser': specifier: ~1.19.2 version: 1.19.6 @@ -32632,7 +35899,7 @@ importers: specifier: ~1.9.9 version: 1.9.10 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 @@ -32671,7 +35938,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -32680,8 +35947,8 @@ importers: ../../server/indexer: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../plugins/attachment @@ -32689,62 +35956,62 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/card '@hcengineering/communication-rest-client': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/rest-client '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-shared': - specifier: ^0.7.11 - version: 0.7.11 + specifier: workspace:^0.7.11 + version: link:../../foundations/communication/packages/shared '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../../plugins/drive '@hcengineering/hulylake-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/hulylake-client '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/query': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/query '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/storage': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/storage '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/text-markdown fast-equals: specifier: ^5.2.2 version: 5.3.3 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 @@ -32778,7 +36045,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32834,11 +36101,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/communication-assets '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact @@ -32852,8 +36119,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/controlled-documents-assets '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/document': specifier: workspace:^0.7.0 version: link:../../plugins/document @@ -32891,8 +36158,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/inventory-assets '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/server/packages/kafka '@hcengineering/lead': specifier: workspace:^0.7.0 version: link:../../plugins/lead @@ -32918,8 +36185,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/mail-assets '@hcengineering/middleware': - specifier: ^0.7.21 - version: 0.7.21 + specifier: workspace:^0.7.21 + version: link:../../foundations/server/packages/middleware '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification @@ -32933,8 +36200,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/onboard-assets '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../plugins/preference @@ -32960,8 +36227,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/request-assets '@hcengineering/server': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/server '@hcengineering/server-activity': specifier: workspace:^0.7.0 version: link:../../server-plugins/activity @@ -33017,8 +36284,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../server-plugins/controlled-documents-resources '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-document': specifier: workspace:^0.7.0 version: link:../../server-plugins/document @@ -33134,8 +36401,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../server-plugins/time-resources '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/server-tracker': specifier: workspace:^0.7.0 version: link:../../server-plugins/tracker @@ -33228,13 +36495,13 @@ importers: version: link:../../plugins/workbench-assets devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -33246,7 +36513,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -33273,7 +36540,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -33282,62 +36549,62 @@ importers: ../../server/tool: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/collaboration': - specifier: ^0.7.16 - version: 0.7.16(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/collaboration '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/importer': specifier: workspace:^0.7.0 version: link:../../packages/importer '@hcengineering/minio': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/minio '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/mongo '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rank '@hcengineering/server': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/server '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/text '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/text-markdown fast-equals: specifier: ^5.2.2 version: 5.3.3 @@ -33352,8 +36619,8 @@ importers: version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -33361,13 +36628,13 @@ importers: specifier: ^4.0.9 version: 4.0.9 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -33398,7 +36665,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33406,35 +36673,35 @@ importers: ../../server/workspace-service: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/mongo '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/postgres': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/server/packages/postgres '@hcengineering/server-backup': specifier: workspace:^0.7.0 version: link:../backup '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../server-plugins/notification @@ -33442,11 +36709,11 @@ importers: specifier: workspace:^0.7.0 version: link:../server-pipeline '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/server-tool': specifier: workspace:^0.7.0 version: link:../tool @@ -33464,8 +36731,8 @@ importers: version: 12.0.1 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -33482,7 +36749,7 @@ importers: specifier: ^5.0.0 version: 5.0.1 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -33494,7 +36761,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -33521,7 +36788,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -33533,14 +36800,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../../server/account '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/ai-bot': specifier: workspace:^0.7.0 version: link:../../../plugins/ai-bot '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../../plugins/attachment @@ -33551,17 +36818,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/chunter '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client-resources '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/document': specifier: workspace:^0.7.0 version: link:../../../plugins/document @@ -33569,8 +36836,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/love '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/mongo '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../../plugins/notification @@ -33578,41 +36845,41 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/openai '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/rank': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/rank '@hcengineering/retry': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/retry '@hcengineering/server-ai-bot': specifier: workspace:^0.7.0 version: link:../../../server-plugins/ai-bot '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../../plugins/setting '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/text '@hcengineering/text-html': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/text-html '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../../foundations/core/packages/text-markdown '@hcengineering/workbench': specifier: workspace:^0.7.0 version: link:../../../plugins/workbench @@ -33648,8 +36915,8 @@ importers: version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -33663,13 +36930,13 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -33678,7 +36945,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -33708,7 +36975,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -33717,8 +36984,8 @@ importers: ../../services/analytics-collector/pod-analytics-collector: dependencies: '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics '@hcengineering/analytics-collector': specifier: workspace:^0.7.0 version: link:../../../plugins/analytics-collector @@ -33726,23 +36993,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/analytics-collector-assets '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token cors: specifier: ^2.8.5 version: 2.8.5 @@ -33757,8 +37024,8 @@ importers: version: 4.3.29 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -33772,7 +37039,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -33781,7 +37048,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -33811,7 +37078,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -33820,32 +37087,32 @@ importers: ../../services/backup/backup-api-pod: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-backup': specifier: workspace:^0.7.0 version: link:../../../server/backup '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token cors: specifier: ^2.8.5 version: 2.8.5 @@ -33866,8 +37133,8 @@ importers: version: 0.34.5 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -33887,10 +37154,10 @@ importers: specifier: ~1.9.9 version: 1.9.10 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -33899,7 +37166,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -33929,7 +37196,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -33938,35 +37205,35 @@ importers: ../../services/billing/pod-billing: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/datalake': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/datalake '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token cors: specifier: ^2.8.5 version: 2.8.5 @@ -33990,8 +37257,8 @@ importers: version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -34008,7 +37275,7 @@ importers: specifier: ~1.9.9 version: 1.9.10 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/on-headers': specifier: ^1.0.2 @@ -34017,7 +37284,7 @@ importers: specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -34029,7 +37296,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -34059,7 +37326,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -34068,11 +37335,11 @@ importers: ../../services/calendar/pod-calendar: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../../plugins/attachment @@ -34080,17 +37347,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/calendar '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client-resources '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/integration-client': specifier: workspace:^0.7.0 version: link:../../../packages/integration-client @@ -34098,23 +37365,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../../packages/kvs-client '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../../plugins/setting '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/text cors: specifier: ^2.8.5 version: 2.8.5 @@ -34144,8 +37411,8 @@ importers: version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -34159,10 +37426,10 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -34171,7 +37438,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -34201,7 +37468,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -34210,17 +37477,17 @@ importers: ../../services/calendar/pod-calendar-mailer: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/api-client': - specifier: ^0.7.18 - version: 0.7.18(bufferutil@4.0.9)(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/api-client '@hcengineering/calendar': specifier: workspace:^0.7.0 version: link:../../../plugins/calendar @@ -34228,11 +37495,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/server/packages/kafka '@hcengineering/love': specifier: workspace:^0.7.0 version: link:../../../plugins/love @@ -34240,21 +37507,21 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token dotenv: specifier: ^16.4.5 version: 16.6.1 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -34262,7 +37529,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -34274,7 +37541,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -34304,7 +37571,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -34322,32 +37589,32 @@ importers: specifier: ^3.738.0 version: 3.937.0 '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/server/packages/kafka '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token '@smithy/node-http-handler': specifier: ^4.0.2 version: 4.4.5 @@ -34380,8 +37647,8 @@ importers: version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -34401,7 +37668,7 @@ importers: specifier: ~1.9.9 version: 1.9.10 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/on-headers': specifier: ^1.0.2 @@ -34410,7 +37677,7 @@ importers: specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -34419,7 +37686,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -34449,7 +37716,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -34461,14 +37728,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../../server/account '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/document': specifier: workspace:^0.7.0 version: link:../../../plugins/document @@ -34488,23 +37755,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/text archiver: specifier: ^7.0.1 version: 7.0.1 @@ -34534,8 +37801,8 @@ importers: version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -34555,13 +37822,13 @@ importers: specifier: ^4.0.9 version: 4.0.9 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -34570,7 +37837,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -34600,7 +37867,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -34621,11 +37888,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../../plugins/preference @@ -34649,8 +37916,8 @@ importers: version: link:../../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -34683,7 +37950,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34694,17 +37961,17 @@ importers: specifier: workspace:^0.7.0 version: link:../github '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -34735,7 +38002,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34743,8 +38010,8 @@ importers: ../../services/github/github-resources: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/activity': specifier: workspace:^0.7.0 version: link:../../../plugins/activity @@ -34752,8 +38019,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/activity-resources '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics '@hcengineering/attachment-resources': specifier: workspace:^0.7.0 version: link:../../../plugins/attachment-resources @@ -34764,8 +38031,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/chunter-resources '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../../plugins/contact @@ -34773,8 +38040,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/contact-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/diffview': specifier: workspace:^0.7.0 version: link:../../../plugins/diffview @@ -34791,8 +38058,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../packages/panel '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../../plugins/preference @@ -34809,8 +38076,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/task '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/text '@hcengineering/text-editor': specifier: workspace:^0.7.0 version: link:../../../plugins/text-editor @@ -34840,8 +38107,8 @@ importers: version: 4.2.20 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -34915,8 +38182,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/github': specifier: workspace:^0.7.0 version: link:../github @@ -34924,8 +38191,8 @@ importers: specifier: workspace:^0.7.0 version: link:../github-resources '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/model '@hcengineering/model-activity': specifier: workspace:^0.7.0 version: link:../../../models/activity @@ -34969,8 +38236,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../../plugins/preference @@ -34984,8 +38251,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/task '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/text '@hcengineering/time': specifier: workspace:^0.7.0 version: link:../../../plugins/time @@ -35000,13 +38267,13 @@ importers: version: link:../../../plugins/view devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -35037,7 +38304,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35048,8 +38315,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../server/account '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/activity': specifier: workspace:^0.7.0 version: link:../../../plugins/activity @@ -35057,11 +38324,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/activity-assets '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../../plugins/attachment @@ -35093,14 +38360,14 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/chunter-assets '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client-resources '@hcengineering/collaborator-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/collaborator-client '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../../plugins/contact @@ -35108,8 +38375,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/contact-assets '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/document': specifier: workspace:^0.7.0 version: link:../../../plugins/document @@ -35159,11 +38426,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/login-assets '@hcengineering/minio': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/minio '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/mongo '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../../plugins/notification @@ -35171,8 +38438,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/notification-assets '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/preference': specifier: workspace:^0.7.0 version: link:../../../plugins/preference @@ -35180,8 +38447,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/preference-assets '@hcengineering/query': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/query '@hcengineering/recruit': specifier: workspace:^0.7.0 version: link:../../../plugins/recruit @@ -35195,20 +38462,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/request-assets '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-guest-resources': specifier: workspace:^0.7.0 version: link:../../../server-plugins/guest-resources '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../../plugins/setting @@ -35246,11 +38513,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/templates-assets '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/text '@hcengineering/text-markdown': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.20 + version: link:../../../foundations/core/packages/text-markdown '@hcengineering/time': specifier: workspace:^0.7.0 version: link:../../../plugins/time @@ -35379,8 +38646,8 @@ importers: version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@octokit/webhooks-types': specifier: ^7.3.1 version: 7.6.1 @@ -35406,13 +38673,13 @@ importers: specifier: ~13.0.0 version: 13.0.9 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -35424,7 +38691,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -35457,7 +38724,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -35466,17 +38733,17 @@ importers: ../../services/github/server-github: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/github': specifier: workspace:^0.7.0 version: link:../github '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-notification': specifier: workspace:^0.7.0 version: link:../../../server-plugins/notification @@ -35485,13 +38752,13 @@ importers: version: link:../../../plugins/time devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -35522,7 +38789,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35533,23 +38800,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/github': specifier: workspace:^0.7.0 version: link:../github '@hcengineering/model': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/model '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-github': specifier: workspace:^0.7.0 version: link:../server-github @@ -35564,13 +38831,13 @@ importers: version: link:../../../plugins/tracker devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -35601,7 +38868,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35615,17 +38882,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/github': specifier: workspace:^0.7.0 version: link:../github '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-github': specifier: workspace:^0.7.0 version: link:../server-github @@ -35637,8 +38904,8 @@ importers: version: link:../../../plugins/tracker devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -35671,7 +38938,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35679,14 +38946,14 @@ importers: ../../services/gmail/pod-gmail: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/api-client': - specifier: ^0.7.18 - version: 0.7.18(bufferutil@4.0.9)(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/api-client '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../../plugins/attachment @@ -35697,23 +38964,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/chat '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client-resources '@hcengineering/communication-rest-client': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../../foundations/communication/packages/rest-client '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../../foundations/communication/packages/sdk-types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/gmail': specifier: workspace:^0.7.0 version: link:../../../plugins/gmail @@ -35721,8 +38988,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../packages/integration-client '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/server/packages/kafka '@hcengineering/kvs-client': specifier: workspace:^0.7.0 version: link:../../../packages/kvs-client @@ -35730,20 +38997,20 @@ importers: specifier: workspace:^0.7.0 version: link:../../mail/mail-common '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../../plugins/setting @@ -35785,8 +39052,8 @@ importers: version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -35800,7 +39067,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/sanitize-html': specifier: ^2.15.0 @@ -35809,7 +39076,7 @@ importers: specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -35821,7 +39088,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -35851,7 +39118,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) ts-node-dev: specifier: ^2.0.0 @@ -35863,11 +39130,11 @@ importers: ../../services/love: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../plugins/attachment @@ -35875,17 +39142,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../packages/billing-client '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/datalake': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/datalake '@hcengineering/drive': specifier: workspace:^0.7.0 version: link:../../plugins/drive @@ -35893,23 +39160,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/love '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/s3': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/s3 '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token cors: specifier: ^2.8.5 version: 2.8.5 @@ -35933,8 +39200,8 @@ importers: version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -35948,13 +39215,13 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -35963,7 +39230,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -35993,7 +39260,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -36002,11 +39269,11 @@ importers: ../../services/mail/mail-common: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/api-client': - specifier: ^0.7.18 - version: 0.7.18(bufferutil@4.0.9)(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/api-client '@hcengineering/card': specifier: workspace:^0.7.0 version: link:../../../plugins/card @@ -36014,26 +39281,26 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/chat '@hcengineering/communication-rest-client': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../../foundations/communication/packages/rest-client '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../../foundations/communication/packages/sdk-types '@hcengineering/communication-shared': - specifier: ^0.7.11 - version: 0.7.11 + specifier: workspace:^0.7.11 + version: link:../../../foundations/communication/packages/shared '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/server/packages/kafka '@hcengineering/kvs-client': specifier: workspace:^0.7.0 version: link:../../../packages/kvs-client @@ -36041,11 +39308,11 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/mail '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage image-size: specifier: ^1.1.1 version: 1.2.1 @@ -36069,8 +39336,8 @@ importers: version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -36084,7 +39351,7 @@ importers: specifier: ~13.0.0 version: 13.0.9 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/sanitize-html': specifier: ^2.15.0 @@ -36102,7 +39369,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -36132,7 +39399,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -36144,17 +39411,17 @@ importers: specifier: ^3.738.0 version: 3.936.0 '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@types/nodemailer': specifier: ^6.4.17 version: 6.4.21 @@ -36175,8 +39442,8 @@ importers: version: 6.10.1 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -36190,7 +39457,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -36202,7 +39469,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -36232,7 +39499,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -36241,14 +39508,14 @@ importers: ../../services/mail/pod-mail-worker: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/api-client': - specifier: ^0.7.18 - version: 0.7.18(bufferutil@4.0.9)(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/api-client '@hcengineering/card': specifier: workspace:^0.7.0 version: link:../../../plugins/card @@ -36256,23 +39523,23 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/chat '@hcengineering/communication-rest-client': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../../foundations/communication/packages/rest-client '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../../foundations/communication/packages/sdk-types '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/server/packages/kafka '@hcengineering/kvs-client': specifier: workspace:^0.7.0 version: link:../../../packages/kvs-client @@ -36283,17 +39550,17 @@ importers: specifier: workspace:^0.7.0 version: link:../mail-common '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token cors: specifier: ^2.8.5 version: 2.8.5 @@ -36320,8 +39587,8 @@ importers: version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -36335,7 +39602,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/sanitize-html': specifier: ^2.15.0 @@ -36356,7 +39623,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -36386,7 +39653,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -36395,23 +39662,23 @@ importers: ../../services/notification/pod-notification: dependencies: '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token cors: specifier: ^2.8.5 version: 2.8.5 @@ -36426,8 +39693,8 @@ importers: version: 3.6.7 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -36441,7 +39708,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/web-push': specifier: ^3.6.4 @@ -36456,7 +39723,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -36486,7 +39753,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -36495,29 +39762,29 @@ importers: ../../services/payment/pod-payment: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token '@polar-sh/sdk': specifier: ^0.37.0 version: 0.37.0 @@ -36547,8 +39814,8 @@ importers: version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -36568,7 +39835,7 @@ importers: specifier: ~1.9.9 version: 1.9.10 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/on-headers': specifier: ^1.0.2 @@ -36577,7 +39844,7 @@ importers: specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -36586,7 +39853,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -36616,7 +39883,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -36625,26 +39892,26 @@ importers: ../../services/print/pod-print: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token cors: specifier: ^2.8.5 version: 2.8.5 @@ -36665,8 +39932,8 @@ importers: version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -36680,10 +39947,10 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -36692,7 +39959,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -36722,7 +39989,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -36731,44 +39998,44 @@ importers: ../../services/process: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/api-client': - specifier: ^0.7.18 - version: 0.7.18(bufferutil@4.0.9)(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/api-client '@hcengineering/card': specifier: workspace:^0.7.0 version: link:../../plugins/card '@hcengineering/collaborator-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/collaborator-client '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/server/packages/kafka '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/process': specifier: workspace:^0.7.0 version: link:../../plugins/process '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-process': specifier: workspace:^0.7.0 version: link:../../server-plugins/process @@ -36776,8 +40043,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../server-plugins/process-resources '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@temporalio/client': specifier: 1.12.3 version: 1.12.3 @@ -36786,8 +40053,8 @@ importers: version: 16.6.1 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -36795,13 +40062,13 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -36810,7 +40077,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -36840,7 +40107,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -36849,59 +40116,59 @@ importers: ../../services/rating: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-server': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/server '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/server/packages/kafka '@hcengineering/middleware': - specifier: ^0.7.21 - version: 0.7.21 + specifier: workspace:^0.7.21 + version: link:../../foundations/server/packages/middleware '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/mongo '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/postgres': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/server/packages/postgres '@hcengineering/rating': specifier: workspace:^0.7.0 version: link:../../plugins/rating '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-pipeline': specifier: workspace:^0.7.0 version: link:../../server/server-pipeline '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@koa/cors': specifier: ^5.0.0 version: 5.0.0 @@ -36925,8 +40192,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../models/all '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -36943,7 +40210,7 @@ importers: specifier: ^5.0.0 version: 5.0.1 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -36955,7 +40222,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -36982,7 +40249,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -36994,20 +40261,20 @@ importers: specifier: ^0.6.0 version: 0.6.0 '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token body-parser: specifier: ^1.20.3 version: 1.20.3 @@ -37055,8 +40322,8 @@ importers: version: 0.34.5 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/body-parser': specifier: ~1.19.2 version: 1.19.6 @@ -37082,7 +40349,7 @@ importers: specifier: ~1.9.9 version: 1.9.10 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -37091,7 +40358,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -37124,7 +40391,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) ts-node-dev: specifier: ^2.0.0 @@ -37136,38 +40403,38 @@ importers: ../../services/sign/pod-sign: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/server '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token '@signpdf/placeholder-pdf-lib': specifier: ^3.2.4 version: 3.2.6(pdf-lib@1.17.1) @@ -37200,8 +40467,8 @@ importers: version: 1.17.1 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -37215,7 +40482,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -37224,7 +40491,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -37254,7 +40521,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -37263,20 +40530,20 @@ importers: ../../services/telegram-bot/pod-telegram-bot: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../../foundations/core/packages/account-client '@hcengineering/activity': specifier: workspace:^0.7.0 version: link:../../../plugins/activity '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/api-client': - specifier: ^0.7.18 - version: 0.7.18(bufferutil@4.0.9)(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/api-client '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../../plugins/attachment @@ -37284,44 +40551,44 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/chunter '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client-resources '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/server/packages/kafka '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/mongo '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../../plugins/notification '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage '@hcengineering/server-telegram': specifier: workspace:^0.7.0 version: link:../../../server-plugins/telegram '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../../plugins/setting @@ -37332,8 +40599,8 @@ importers: specifier: workspace:^0.7.0 version: link:../../../plugins/telegram-assets '@hcengineering/text': - specifier: ^0.7.18 - version: 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/text '@telegraf/entity': specifier: ^0.5.0 version: 0.5.0 @@ -37363,8 +40630,8 @@ importers: version: 4.16.3(encoding@0.1.13) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -37378,13 +40645,13 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/otp-generator': specifier: ^4.0.2 version: 4.0.2 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -37393,7 +40660,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -37423,7 +40690,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -37432,41 +40699,41 @@ importers: ../../services/telegram/pod-telegram: dependencies: '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/analytics-service '@hcengineering/attachment': specifier: workspace:^0.7.0 version: link:../../../plugins/attachment '@hcengineering/client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/client-resources '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../../foundations/core/packages/core '@hcengineering/mongo': - specifier: ^0.7.16 - version: 0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/mongo '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../../foundations/core/packages/platform '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/server/packages/core '@hcengineering/server-storage': - specifier: ^0.7.16 - version: 0.7.16 + specifier: workspace:^0.7.16 + version: link:../../../foundations/server/packages/server-storage '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../../foundations/core/packages/token '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../../../plugins/setting @@ -37508,8 +40775,8 @@ importers: version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -37526,13 +40793,13 @@ importers: specifier: ^3.0.1 version: 3.0.4 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -37544,7 +40811,7 @@ importers: specifier: ^6.10.3 version: 6.10.4 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -37569,7 +40836,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) ts-node-dev: specifier: ^2.0.0 version: 2.0.0(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) @@ -37580,17 +40847,17 @@ importers: ../../services/translate: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/analytics-service': - specifier: ^0.7.17 - version: 0.7.17(encoding@0.1.13) + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics-service '@hcengineering/api-client': - specifier: ^0.7.18 - version: 0.7.18(bufferutil@4.0.9)(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/api-client '@hcengineering/billing-client': specifier: workspace:^0.7.0 version: link:../../packages/billing-client @@ -37598,41 +40865,41 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/card '@hcengineering/communication-sdk-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/sdk-types '@hcengineering/communication-shared': - specifier: ^0.7.11 - version: 0.7.11 + specifier: workspace:^0.7.11 + version: link:../../foundations/communication/packages/shared '@hcengineering/communication-types': - specifier: ^0.7.12 - version: 0.7.12 + specifier: workspace:^0.7.12 + version: link:../../foundations/communication/packages/types '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/hulylake-client': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/hulylake-client '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/server/packages/kafka '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/retry': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/retry '@hcengineering/server-client': - specifier: ^0.7.16 - version: 0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.16 + version: link:../../foundations/server/packages/client '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token dotenv: specifier: ^16.4.5 version: 16.6.1 @@ -37641,8 +40908,8 @@ importers: version: 4.104.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@tsconfig/node16': specifier: ^1.0.4 version: 1.0.4 @@ -37650,13 +40917,13 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -37665,7 +40932,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -37695,7 +40962,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -37704,17 +40971,17 @@ importers: ../../services/worker: dependencies: '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/kafka': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/server/packages/kafka '@hcengineering/process': specifier: workspace:^0.7.0 version: link:../../plugins/process '@hcengineering/server-core': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/server/packages/core '@temporalio/worker': specifier: 1.12.3 version: 1.12.3(esbuild@0.25.12) @@ -37723,8 +40990,8 @@ importers: version: 1.12.3 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@hcengineering/server-process': specifier: workspace:^0.7.0 version: link:../../server-plugins/process @@ -37735,13 +41002,13 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/uuid': specifier: ^8.3.1 version: 8.3.4 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -37753,7 +41020,7 @@ importers: specifier: ~7.0.3 version: 7.0.3 esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -37783,7 +41050,7 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: - specifier: ^10.8.0 + specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3) typescript: specifier: ^5.9.3 @@ -37795,17 +41062,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../server/account '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token cross-env: specifier: ~7.0.3 version: 7.0.3 @@ -37817,8 +41084,8 @@ importers: specifier: ^8.4.1 version: 8.4.1 '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@playwright/test': specifier: ^1.48.2 version: 1.56.1 @@ -37826,7 +41093,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -37865,14 +41132,14 @@ importers: ../../ws-tests/api-tests: dependencies: '@hcengineering/account-client': - specifier: ^0.7.19 - version: 0.7.19 + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/account-client '@hcengineering/analytics': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/analytics '@hcengineering/api-client': - specifier: ^0.7.18 - version: 0.7.18(bufferutil@4.0.9)(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(utf-8-validate@6.0.5) + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/api-client '@hcengineering/chunter': specifier: workspace:^0.7.0 version: link:../../plugins/chunter @@ -37880,17 +41147,17 @@ importers: specifier: workspace:^0.7.0 version: link:../../plugins/contact '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core '@hcengineering/platform': - specifier: ^0.7.18 - version: 0.7.18 + specifier: workspace:^0.7.18 + version: link:../../foundations/core/packages/platform '@hcengineering/rpc': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/rpc '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/token '@hcengineering/task': specifier: workspace:^0.7.0 version: link:../../plugins/task @@ -37911,8 +41178,8 @@ importers: version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@types/body-parser': specifier: ~1.19.2 version: 1.19.6 @@ -37932,10 +41199,10 @@ importers: specifier: ~1.9.9 version: 1.9.10 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@types/ws': - specifier: ^8.5.11 + specifier: ^8.5.12 version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -37966,7 +41233,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -37974,11 +41241,11 @@ importers: ../../ws-tests/sanity: dependencies: '@hcengineering/client-resources': - specifier: ^0.7.17 - version: 0.7.17 + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/client-resources '@hcengineering/core': - specifier: ^0.7.22 - version: 0.7.22 + specifier: workspace:^0.7.22 + version: link:../../foundations/core/packages/core cross-env: specifier: ~7.0.3 version: 7.0.3 @@ -37990,8 +41257,8 @@ importers: specifier: ^8.4.1 version: 8.4.1 '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@hcengineering/tests-sanity': specifier: workspace:^0.7.0 version: link:../../tests/sanity @@ -38002,7 +41269,7 @@ importers: specifier: ^29.5.5 version: 29.5.14 '@types/node': - specifier: ^22.15.29 + specifier: ^22.18.1 version: 22.19.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 @@ -38041,8 +41308,8 @@ importers: ../scripts: devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) @@ -38050,7 +41317,7 @@ importers: specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) esbuild: - specifier: ^0.25.9 + specifier: ^0.25.10 version: 0.25.12 eslint: specifier: ^8.54.0 @@ -38764,154 +42031,6 @@ packages: '@hapi/bourne@3.0.0': resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} - '@hcengineering/account-client@0.7.19': - resolution: {integrity: sha512-mujENsaMw2Hno4IAAHWIuD6T9wbQXByAnW5MU1+yrGwUOWD08OgWv5m/FgCBrj2JEmF1Pdq/yz4tM/vOpu36ZA==} - - '@hcengineering/analytics-service@0.7.17': - resolution: {integrity: sha512-y70WkyyNYehR6C2GmzUH5ITNpPfCa6GJusmdQRZEx3EkiJdQPSBxTJ7nh53Tfzh+rvOdg4lDTaTscEUbPjPKow==} - - '@hcengineering/analytics@0.7.17': - resolution: {integrity: sha512-AzB2DvmDctEJge8VeAHS3YyRYij/QQlARxa2OCNiY+Vv+2+OsQk0NZsw1yqZqsMkVJ40FTUrzOkI7UdM6RPO1Q==} - - '@hcengineering/api-client@0.7.18': - resolution: {integrity: sha512-QALxpP2a4IgibHsnY/Qn+RV4/ITXmXG90CiiggHyv6BkgPUS0E0viBr8WhAh0S+flVMgybTmaxxpXX3OzvPCaA==} - - '@hcengineering/client-resources@0.7.17': - resolution: {integrity: sha512-Tu7G11ezraEnHUhJAhcSFpsH67oofmlDdozUQ/ioEVZGwDpmzolRTYo3us/VCOKAJ7gDQiNuA+95V8J9yLpRlQ==} - - '@hcengineering/client@0.7.17': - resolution: {integrity: sha512-3SjW0e/pPa/qQNqpGuh6A+Y5XN0WQ2EMgUZaXjY2Xmchpna7Xp6hYDvzSL4yPA4tZSGE8n0gu7FK8LRamMdrUg==} - - '@hcengineering/collaboration@0.7.16': - resolution: {integrity: sha512-e4RKys+vUq+fCz48uB2HADa/85q4kt5ojZrUw/dOf4DGw8bMbwg20MktaVwmzzgsYbQK2edrUsstRqlrfMYx5A==} - - '@hcengineering/collaborator-client@0.7.17': - resolution: {integrity: sha512-GPv6u0CGuu4IOJmiKq/omlH4xdDkOn6HV7oDhNskOohpoNuui+ucNeUj5lq6a4SQxQfAjwf6f44K53ZJe/ygIw==} - - '@hcengineering/communication-client-query@0.7.11': - resolution: {integrity: sha512-D3igR77WEZVcRYks6OrpBK5Q44/QMimanIa0CZsBV3N0uzGW6kQf42oljgArJDgGVShrFhSsvvteW1XDmfSklA==} - - '@hcengineering/communication-cockroach@0.7.11': - resolution: {integrity: sha512-S0fUx1OUT0b0gVBPUF8weBupwBsjm55tvzQsjan41z9CSl0P9EazSnFRakii5QfDa/Z2De1+20aOhYD0BACPKg==} - - '@hcengineering/communication-query@0.7.11': - resolution: {integrity: sha512-FF0rJJ3xy43fbi/ktFQtq7W7c/+4kh0tMTJ3qEr56iXlnaik5Y9h46G5NRMc4HAoL8v1MmiSNxXGjUhqZZpS6A==} - - '@hcengineering/communication-rest-client@0.7.12': - resolution: {integrity: sha512-7GzqdX+ZHS/M+gQVqcO7fhivo5rzS/A24TRi2ys+/n5huZtHeSL6s1PjLaM67kV7fvOCtYc3L2wq/h+2Ktc5kQ==} - - '@hcengineering/communication-sdk-types@0.7.12': - resolution: {integrity: sha512-9kkhqsfcKRZrXzVxLTrDlss55tglHDCen5xJFra27IqlfbDoRMywDa9DtwOcz4dTc85+UBhvYsyjxDiOzuPJdg==} - - '@hcengineering/communication-server@0.7.12': - resolution: {integrity: sha512-fy38TI4bKHtHFln/nR/nqYvRm5/d2bO/07+jnJRYzKSZnKLh9wzY1QzwVKJD33fzuev5BPGyqqlMDgrgBbejZA==} - - '@hcengineering/communication-shared@0.7.11': - resolution: {integrity: sha512-fVpsL6wvXrYKWh2oUUknEAgydFQY9k9mf3Hu3llUmcSjwPJ0p5JxD7+pvNCTSwHgdo7BX5KRutoxLa72803Mxg==} - - '@hcengineering/communication-types@0.7.12': - resolution: {integrity: sha512-rjr6AWApjrUd1tHvPBQJhriqrtD09q0ydRfTMupUGJOZJcfYddfAwZHPuSVxJz7oUR8d82aiEoa3UpvWyW9BIQ==} - - '@hcengineering/core@0.7.22': - resolution: {integrity: sha512-DO5/8ZMLjIHw1xINnr+ASD2Fk/ne5/zJC7Qp0orm8jNdxaBalfv6sX+I2rLXazWzLjH7XYniHPUHdifuXOBbWA==} - - '@hcengineering/datalake@0.7.16': - resolution: {integrity: sha512-25GLaDpG2VaDNn4JuCmBkkbcuNpjP/EEr/tjt2yU5LgQZxie+h5gRznhVno//OkU1HB7G5xe5KeLbal64MUvng==} - - '@hcengineering/elastic@0.7.16': - resolution: {integrity: sha512-ekntM+z0ytzCnA7yUwCdYkY/KcedFYra78TFLHfeqAMQmgcqo7bfYyKdczFluxO/yyw2i+zOar7K22MfkluQGA==} - - '@hcengineering/hulylake-client@0.7.17': - resolution: {integrity: sha512-TPqdc5nd7xV73Y1B2EJ7GxpWazp9ACV1AqHlm6ebYwU1CzXQvbuJxF68itbHmPiNiCmmuiDruJmjnyLGwL18Kw==} - - '@hcengineering/hulylake@0.7.16': - resolution: {integrity: sha512-SbCKIfx5CvTROQUkIx90Xvj9/T1ldFGYjoeTc9gGE0wYtfGlgcioe26oVeaFtnneiYWCcqUHiDgAO5X7bkUr5Q==} - - '@hcengineering/kafka@0.7.18': - resolution: {integrity: sha512-AtZzIhG8LfcUDScmjemAYRgizaaJw8Kn1Na/UANwPWhJiNmBSVLY5dbRPW3sSuGaI2HjOKOjAUArUEGCJMVCYw==} - - '@hcengineering/measurements-otlp@0.7.17': - resolution: {integrity: sha512-Yo0N4bUYJT9usEkHvEmNbcWbHyaBufJwwY+RC9G44mTr78Z1ch5gmpFBkvZPSYmKnEIkSHThrA72D3HdAWTqEg==} - - '@hcengineering/measurements@0.7.18': - resolution: {integrity: sha512-4MxYthJhNWPfR389lN5cIFtrhzgmhi39Fx/dqIOOYF+6g0WWA6GtWXi+ORRM8l/AWKSastZqsWxMdBbT+0htdg==} - - '@hcengineering/middleware@0.7.21': - resolution: {integrity: sha512-xQItsrMQCo430jBsSIb5r59JiooRWnA1OD3w5w4ny2W4T+rH3yPZsjl0bX4viOFMv4l7nQOoTs4Bjpeo2MDEzw==} - - '@hcengineering/minio@0.7.16': - resolution: {integrity: sha512-ce2fs/fE2q9BFxNwDZ8HLS7QrdzVGHvqnq2mdxBDzztad2np3Np+Gx1D1VMiIAoiWWK3v9la2NjyocUJ7GYehA==} - - '@hcengineering/model@0.7.17': - resolution: {integrity: sha512-V4BTMfqscv/LwUJoKiqdOJlkt5SkQU7cTF3ekC69IS/hPGuYjLkEU+MlvIzxYIofrY8QOCmPK4Ik7/jr6K/hMg==} - - '@hcengineering/mongo@0.7.16': - resolution: {integrity: sha512-+1Py7PSosweUEyCJM9lWhA9TD/wSIUtN7D37kJzersIQS83uhJ3m/O+WeZeaTSPylhX745liqq5ZhsGDJRSypQ==} - - '@hcengineering/platform-rig@0.7.19': - resolution: {integrity: sha512-3Fi5nU+nEPjzzcm3FIfEZU+ZgdkiSfL46ojbj42jdXnWMkxew2aZZVtfmIpPnMUP9AMZlj2/EA+yOcMzfY/VmA==} - hasBin: true - - '@hcengineering/platform@0.7.18': - resolution: {integrity: sha512-IIeNkXGtCIEvLbL/4O+LHGIHoNPWm7fQTqCYoEYytr6WrLrEr4OZKB/Dpd53rOIQegU+Dh5KwWBh1c/6UAqDHw==} - - '@hcengineering/postgres-base@0.7.17': - resolution: {integrity: sha512-ex5Xtsce/wa4xdY6dDlKyWaFsNQlgHVVcD0R9BF7hOLDkTDbtkzpT+syYpCJSXCfT4MoA9x3xDcGAD5geDYhrg==} - - '@hcengineering/postgres@0.7.19': - resolution: {integrity: sha512-jWtSJfYlrp51Ua7D1E7LwZ/NBOktlHOuB26n7ouCee/rgHm6cyFMpqYVWUKOZZMwQ8hRuqfUA2qnHBReQDMung==} - - '@hcengineering/query@0.7.17': - resolution: {integrity: sha512-dOQbLwEHIp5D4J73I96LKeCIb5Am12jXAmrqpoMMmnA8u6UZylJVT8Vn9HwfPTUTVXmYnv9QxUNPKEn/MpaOAw==} - - '@hcengineering/rank@0.7.17': - resolution: {integrity: sha512-olGMdtWWaRvz3uDnFmn8GyCW+/yqfN7fqR3uR8F/HlecBMReoQOBsEQlExJAAU7hdIJPS6u+F3Ge82uaUUWejg==} - - '@hcengineering/retry@0.7.17': - resolution: {integrity: sha512-JPIGoWP3RHTET0laQV4rqlNxjn3tT9O54XNxCxsxhTFF5xeC02XxmW/xV4Gh6QWE5/QCXY5DD3Sz3ild3Cr7Uw==} - - '@hcengineering/rpc@0.7.17': - resolution: {integrity: sha512-DEd8EMc+Zxgnnk7v9ELj8bk0GpwcKG8rgaMDd2ZXk/JUx9l99RJoJ9/5rlSUHGv8SRxTq8DYJR85nU3aasVd9Q==} - - '@hcengineering/s3@0.7.16': - resolution: {integrity: sha512-yY1+EOEtTFbjx3l2XWmKn1Y5p2ZRtC4PElJ2W+dJ/JtbJ+DCwJcFPsM9xWOcklBrJHINXvbBP5U5fY8iDmKpXg==} - - '@hcengineering/server-client@0.7.16': - resolution: {integrity: sha512-dv6VGNSi64cYDKMVIl4hQl8ayh4/LeTcrQHQg3iB+ehCrBssYh5HTaKp19ySS08tMy4ZZb9VzdCeVbgRWwmbBQ==} - - '@hcengineering/server-core@0.7.17': - resolution: {integrity: sha512-p+ijVeqNUG5YKJerjnAXJV8OBLPOAZv7zgKDDKMS9NRDNW7XNDTTmV/7sRPTFFH8AVufa0kSfVvrDHCMOhvr+g==} - - '@hcengineering/server-storage@0.7.16': - resolution: {integrity: sha512-gl0CL/7HG6EgcNQH3U2N5FhfTqLtcraATljhT+BgqTnFtlR1zK3Ni8kCv3bhc1FWjkmcl+0jxN+RTEm4AQ9gNw==} - - '@hcengineering/server-token@0.7.17': - resolution: {integrity: sha512-MDoGzVOTScJXnx2jHL63szclhiqPOKEXk7bmUj6i/HLUQoU6UAl9TTq1Zx/TomyzFQUx8Vkuj0j0bJMUcmLXmQ==} - - '@hcengineering/server@0.7.17': - resolution: {integrity: sha512-Wvnh0KXs8G30ICWpUyyPJkQ7JJe1/OmUFT42wJqJ2sZGjQxH9JrXQXn+8F+bAujboHFs9UfE6zZfG5D8t7ErpQ==} - - '@hcengineering/storage-client@0.7.17': - resolution: {integrity: sha512-rblwAuRZnkbPvpjWFPT7ms7XsFvqz1mUt+aw/ZTBvquNlUTKv0qu61ALCFHlLO2H+QjP9uTi/YdFEV4c5WshXQ==} - - '@hcengineering/storage@0.7.17': - resolution: {integrity: sha512-1njEkieVWcJgZHvHBB7d656x1EgkwpFeYI9PGJPAiIMnONRfP4KsGLEgfT9Hb5QTM451RCjctbDF+ibBpsCBdQ==} - - '@hcengineering/text-core@0.7.18': - resolution: {integrity: sha512-UZ/ZMwVcf60WpnYYcsA7RqYE4JlHnAGVdmUDHX2SYzXXvDIK7deGWD9AKyK4yqI1mb19ccyCr5fGyGoLxx9k/g==} - - '@hcengineering/text-html@0.7.18': - resolution: {integrity: sha512-hGPgptTk2Kfvlg8lAniaFe8jpf3RD4mZD0eL6CoPlXTKylUOklOGHFGWId+QjUYGPLBVkODPErmZxrZKbkRSWg==} - - '@hcengineering/text-markdown@0.7.19': - resolution: {integrity: sha512-KD2HGwoSTRlXyS38mrwpC+pb//mZHj8iGmw5l0g0GY7H/ScT+4HR4gITxTYSoJ7dvvI7jPqYUTs+dj6smMdanQ==} - - '@hcengineering/text-ydoc@0.7.18': - resolution: {integrity: sha512-uww/VyEBGz8HTj9ESG8gIfti4kyWU2aRBGDm2dyljCPTUrgxY348IKP85ZmmxhW8jCGQ7kqTeoXw4xdlRWyCsA==} - - '@hcengineering/text@0.7.18': - resolution: {integrity: sha512-0se5IwEF/7LoHcFqqxtx7WUEc0sfzaytsQeqs15gRfjLx0vJ/XMc/ZAj1lkpeLKMPXRQ0mCwdzMjxVzo3W9XDQ==} - '@hocuspocus/common@2.15.3': resolution: {integrity: sha512-Rzh1HF0a2o/tf90A3w2XNdXd9Ym3aQzMDfD3lAUONCX9B9QOdqdyiORrj6M25QEaJrEIbXFy8LtAFcL0wRdWzA==} @@ -41503,6 +44622,9 @@ packages: '@types/serve-static@1.15.10': resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/snappyjs@0.7.1': + resolution: {integrity: sha512-OxjzJ6cQZstysMh6PEwZWmK9qlKZyezHJKOkcUkZDooSFuog2votUEKkxMaTq51UQF3cJkXKQ+XGlj4FSl8JQQ==} + '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} @@ -41521,6 +44643,9 @@ packages: '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/toposort@2.0.7': + resolution: {integrity: sha512-sQNk65vbC36+UixCkcky+dCr7MlflHcVILg1FVGqlUntsLFv9xd9ToWIVko/gTuin+cVe16t+2YubEFkhnSuPQ==} + '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -42540,6 +45665,10 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + commander@2.11.0: resolution: {integrity: sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==} @@ -49601,484 +52730,6 @@ snapshots: '@hapi/bourne@3.0.0': {} - '@hcengineering/account-client@0.7.19': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - - '@hcengineering/analytics-service@0.7.17(encoding@0.1.13)': - dependencies: - '@hcengineering/analytics': 0.7.17 - '@hcengineering/core': 0.7.22 - '@hcengineering/measurements-otlp': 0.7.17(encoding@0.1.13) - '@hcengineering/platform': 0.7.18 - winston: 3.18.3 - winston-daily-rotate-file: 5.0.0(winston@3.18.3) - transitivePeerDependencies: - - encoding - - supports-color - - '@hcengineering/analytics@0.7.17': - dependencies: - '@hcengineering/platform': 0.7.18 - - '@hcengineering/api-client@0.7.18(bufferutil@4.0.9)(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(utf-8-validate@6.0.5)': - dependencies: - '@hcengineering/account-client': 0.7.19 - '@hcengineering/client': 0.7.17 - '@hcengineering/client-resources': 0.7.17 - '@hcengineering/collaborator-client': 0.7.17 - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/text': 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) - '@hcengineering/text-markdown': 0.7.19 - snappyjs: 0.7.0 - optionalDependencies: - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) - transitivePeerDependencies: - - bufferutil - - prosemirror-inputrules - - prosemirror-model - - prosemirror-state - - prosemirror-view - - utf-8-validate - - '@hcengineering/client-resources@0.7.17': - dependencies: - '@hcengineering/analytics': 0.7.17 - '@hcengineering/client': 0.7.17 - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/rpc': 0.7.17 - snappyjs: 0.7.0 - - '@hcengineering/client@0.7.17': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - - '@hcengineering/collaboration@0.7.16(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/server-core': 0.7.17 - '@hcengineering/text': 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) - '@hcengineering/text-ydoc': 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) - base64-js: 1.5.1 - yjs: 13.6.27 - transitivePeerDependencies: - - prosemirror-inputrules - - prosemirror-model - - prosemirror-state - - prosemirror-view - - '@hcengineering/collaborator-client@0.7.17': - dependencies: - '@hcengineering/core': 0.7.22 - - '@hcengineering/communication-client-query@0.7.11': - dependencies: - '@hcengineering/communication-query': 0.7.11 - '@hcengineering/communication-sdk-types': 0.7.12 - '@hcengineering/communication-types': 0.7.12 - '@hcengineering/hulylake-client': 0.7.17 - fast-equals: 5.3.3 - - '@hcengineering/communication-cockroach@0.7.11': - dependencies: - '@hcengineering/communication-sdk-types': 0.7.12 - '@hcengineering/communication-shared': 0.7.11 - '@hcengineering/communication-types': 0.7.12 - postgres: 3.4.7 - uuid: 8.3.2 - - '@hcengineering/communication-query@0.7.11': - dependencies: - '@hcengineering/communication-sdk-types': 0.7.12 - '@hcengineering/communication-shared': 0.7.11 - '@hcengineering/communication-types': 0.7.12 - '@hcengineering/hulylake-client': 0.7.17 - fast-equals: 5.3.3 - uuid: 8.3.2 - - '@hcengineering/communication-rest-client@0.7.12': - dependencies: - '@hcengineering/communication-sdk-types': 0.7.12 - '@hcengineering/communication-shared': 0.7.11 - '@hcengineering/communication-types': 0.7.12 - '@hcengineering/core': 0.7.22 - snappyjs: 0.7.0 - - '@hcengineering/communication-sdk-types@0.7.12': - dependencies: - '@hcengineering/communication-types': 0.7.12 - '@hcengineering/core': 0.7.22 - - '@hcengineering/communication-server@0.7.12': - dependencies: - '@hcengineering/account-client': 0.7.19 - '@hcengineering/communication-cockroach': 0.7.11 - '@hcengineering/communication-sdk-types': 0.7.12 - '@hcengineering/communication-shared': 0.7.11 - '@hcengineering/communication-types': 0.7.12 - '@hcengineering/core': 0.7.22 - '@hcengineering/hulylake-client': 0.7.17 - '@hcengineering/server-token': 0.7.17 - '@hcengineering/text-core': 0.7.18 - '@hcengineering/text-markdown': 0.7.19 - uuid: 8.3.2 - zod: 3.25.76 - - '@hcengineering/communication-shared@0.7.11': - dependencies: - '@hcengineering/communication-sdk-types': 0.7.12 - '@hcengineering/communication-types': 0.7.12 - '@hcengineering/hulylake-client': 0.7.17 - - '@hcengineering/communication-types@0.7.12': - dependencies: - '@hcengineering/core': 0.7.22 - - '@hcengineering/core@0.7.22': - dependencies: - '@hcengineering/analytics': 0.7.17 - '@hcengineering/measurements': 0.7.18 - '@hcengineering/platform': 0.7.18 - fast-equals: 5.3.3 - - '@hcengineering/datalake@0.7.16': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/server-core': 0.7.17 - '@hcengineering/server-token': 0.7.17 - - '@hcengineering/elastic@0.7.16': - dependencies: - '@elastic/elasticsearch': 7.17.14 - '@hcengineering/analytics': 0.7.17 - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/server-core': 0.7.17 - transitivePeerDependencies: - - supports-color - - '@hcengineering/hulylake-client@0.7.17': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/retry': 0.7.17 - - '@hcengineering/hulylake@0.7.16': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/hulylake-client': 0.7.17 - '@hcengineering/platform': 0.7.18 - '@hcengineering/server-core': 0.7.17 - '@hcengineering/server-token': 0.7.17 - - '@hcengineering/kafka@0.7.18': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/server-core': 0.7.17 - '@hcengineering/storage': 0.7.17 - kafkajs: 2.2.4 - - '@hcengineering/measurements-otlp@0.7.17(encoding@0.1.13)': - dependencies: - '@hcengineering/measurements': 0.7.18 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/auto-instrumentations-node': 0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13) - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/id-generator-aws-xray': 2.0.3(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.2.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - encoding - - supports-color - - '@hcengineering/measurements@0.7.18': {} - - '@hcengineering/middleware@0.7.21': - dependencies: - '@hcengineering/analytics': 0.7.17 - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/query': 0.7.17 - '@hcengineering/server-core': 0.7.17 - fast-equals: 5.3.3 - - '@hcengineering/minio@0.7.16': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/server-core': 0.7.17 - minio: 8.0.6 - - '@hcengineering/model@0.7.17': - dependencies: - '@hcengineering/account-client': 0.7.19 - '@hcengineering/analytics': 0.7.17 - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/rank': 0.7.17 - '@hcengineering/storage': 0.7.17 - fast-equals: 5.3.3 - toposort: 2.0.2 - - '@hcengineering/mongo@0.7.16(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7)': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/server-core': 0.7.17 - bson: 6.10.4 - mongodb: 6.21.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.3.3)(socks@2.8.7) - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - gcp-metadata - - kerberos - - mongodb-client-encryption - - snappy - - socks - - '@hcengineering/platform-rig@0.7.19(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3))': - dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: 0.25.12 - esbuild-plugin-copy: 2.1.1(esbuild@0.25.12) - esbuild-svelte: 0.9.3(esbuild@0.25.12)(svelte@4.2.20) - eslint: 8.57.1 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: 2.32.0(eslint@8.57.1) - eslint-plugin-n: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) - prettier: 3.6.2 - prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@4.2.20) - svelte: 4.2.20 - svelte-eslint-parser: 0.33.1(svelte@4.2.20) - svelte-preprocess: 5.1.4(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.94.2)(svelte@4.2.20)(typescript@5.9.3) - svelte2tsx: 0.7.45(svelte@4.2.20)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - '@babel/core' - - coffeescript - - less - - postcss - - postcss-load-config - - pug - - sass - - stylus - - sugarss - - supports-color - - ts-node - - '@hcengineering/platform@0.7.18': - dependencies: - intl-messageformat: 10.7.18 - - '@hcengineering/postgres-base@0.7.17': - dependencies: - postgres: 3.4.7 - - '@hcengineering/postgres@0.7.19': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/postgres-base': 0.7.17 - '@hcengineering/server-core': 0.7.17 - postgres: 3.4.7 - - '@hcengineering/query@0.7.17': - dependencies: - '@hcengineering/analytics': 0.7.17 - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - fast-equals: 5.3.3 - - '@hcengineering/rank@0.7.17': - dependencies: - '@hcengineering/core': 0.7.22 - lexorank: 1.0.5 - - '@hcengineering/retry@0.7.17': {} - - '@hcengineering/rpc@0.7.17': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - msgpackr: 1.11.5 - - '@hcengineering/s3@0.7.16': - dependencies: - '@aws-sdk/client-s3': 3.937.0 - '@aws-sdk/lib-storage': 3.937.0(@aws-sdk/client-s3@3.937.0) - '@aws-sdk/s3-request-presigner': 3.937.0 - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/server-core': 0.7.17 - '@hcengineering/storage': 0.7.17 - '@smithy/node-http-handler': 4.4.5 - transitivePeerDependencies: - - aws-crt - - '@hcengineering/server-client@0.7.16(bufferutil@4.0.9)(utf-8-validate@6.0.5)': - dependencies: - '@hcengineering/account-client': 0.7.19 - '@hcengineering/client': 0.7.17 - '@hcengineering/client-resources': 0.7.17 - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/server-core': 0.7.17 - '@hcengineering/server-token': 0.7.17 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@hcengineering/server-core@0.7.17': - dependencies: - '@hcengineering/analytics': 0.7.17 - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/query': 0.7.17 - '@hcengineering/rpc': 0.7.17 - '@hcengineering/server-token': 0.7.17 - '@hcengineering/storage': 0.7.17 - fast-equals: 5.3.3 - uuid: 8.3.2 - - '@hcengineering/server-storage@0.7.16': - dependencies: - '@hcengineering/analytics': 0.7.17 - '@hcengineering/core': 0.7.22 - '@hcengineering/datalake': 0.7.16 - '@hcengineering/hulylake': 0.7.16 - '@hcengineering/minio': 0.7.16 - '@hcengineering/platform': 0.7.18 - '@hcengineering/s3': 0.7.16 - '@hcengineering/server-core': 0.7.17 - '@hcengineering/server-token': 0.7.17 - '@hcengineering/storage': 0.7.17 - transitivePeerDependencies: - - aws-crt - - '@hcengineering/server-token@0.7.17': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - jwt-simple: 0.5.6 - uuid: 8.3.2 - - '@hcengineering/server@0.7.17': - dependencies: - '@hcengineering/account-client': 0.7.19 - '@hcengineering/analytics': 0.7.17 - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - '@hcengineering/rpc': 0.7.17 - '@hcengineering/server-core': 0.7.17 - '@hcengineering/server-token': 0.7.17 - utf-8-validate: 6.0.5 - - '@hcengineering/storage-client@0.7.17': - dependencies: - '@hcengineering/core': 0.7.22 - - '@hcengineering/storage@0.7.17': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/platform': 0.7.18 - fast-equals: 5.3.3 - - '@hcengineering/text-core@0.7.18': - dependencies: - '@hcengineering/core': 0.7.22 - fast-equals: 5.3.3 - hash-it: 6.0.1 - - '@hcengineering/text-html@0.7.18': - dependencies: - '@hcengineering/text-core': 0.7.18 - htmlparser2: 9.1.0 - - '@hcengineering/text-markdown@0.7.19': - dependencies: - '@hcengineering/text-core': 0.7.18 - '@hcengineering/text-html': 0.7.18 - fast-equals: 5.3.3 - markdown-it: 14.1.0 - - '@hcengineering/text-ydoc@0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/text': 0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) - '@hcengineering/text-core': 0.7.18 - y-protocols: 1.0.6(yjs@13.6.27) - yjs: 13.6.27 - transitivePeerDependencies: - - prosemirror-inputrules - - prosemirror-model - - prosemirror-state - - prosemirror-view - - '@hcengineering/text@0.7.18(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)': - dependencies: - '@hcengineering/core': 0.7.22 - '@hcengineering/text-core': 0.7.18 - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/extension-blockquote': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-bold': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-bullet-list': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-code': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-code-block': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-document': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-dropcursor': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-gapcursor': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-hard-break': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-heading': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-highlight': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-history': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-horizontal-rule': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-italic': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-link': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-list-item': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-mention': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(@tiptap/suggestion@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)) - '@tiptap/extension-ordered-list': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-paragraph': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-strike': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-table': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-table-cell': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-table-header': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-table-row': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-task-item': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-task-list': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-text': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-text-align': 2.11.9(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-text-style': 2.11.9(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-typography': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-underline': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/html': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - '@tiptap/starter-kit': 2.27.1 - '@tiptap/suggestion': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - fast-equals: 5.3.3 - prosemirror-codemark: 0.4.2(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) - transitivePeerDependencies: - - prosemirror-inputrules - - prosemirror-model - - prosemirror-state - - prosemirror-view - '@hocuspocus/common@2.15.3': dependencies: lib0: 0.2.114 @@ -53250,7 +55901,7 @@ snapshots: '@types/pg-pool@2.0.6': dependencies: - '@types/pg': 8.15.5 + '@types/pg': 8.15.6 '@types/pg@8.15.5': dependencies: @@ -53322,6 +55973,8 @@ snapshots: '@types/node': 22.19.1 '@types/send': 0.17.6 + '@types/snappyjs@0.7.1': {} + '@types/sockjs@0.3.36': dependencies: '@types/node': 22.19.1 @@ -53340,6 +55993,8 @@ snapshots: dependencies: '@types/node': 22.19.1 + '@types/toposort@2.0.7': {} + '@types/tough-cookie@4.0.5': {} '@types/triple-beam@1.3.5': {} @@ -54541,6 +57196,8 @@ snapshots: commander@10.0.1: {} + commander@14.0.2: {} + commander@2.11.0: {} commander@2.20.3: {} @@ -58588,11 +61245,6 @@ snapshots: loader-utils: 2.0.4 webpack: 5.103.0(@swc/core@1.15.3)(esbuild@0.25.12) - node-loader@2.0.0(webpack@5.103.0(@swc/core@1.15.3)): - dependencies: - loader-utils: 2.0.4 - webpack: 5.103.0(@swc/core@1.15.3) - node-loader@2.0.0(webpack@5.103.0): dependencies: loader-utils: 2.0.4 @@ -60779,6 +63431,47 @@ snapshots: esbuild: 0.25.12 jest-util: 30.2.0 + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.5 + '@jest/transform': 29.7.0 + '@jest/types': 30.2.0 + babel-jest: 29.7.0(@babel/core@7.28.5) + esbuild: 0.25.12 + jest-util: 30.2.0 + + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@22.19.1)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.5 + '@jest/transform': 29.7.0 + '@jest/types': 30.2.0 + babel-jest: 29.7.0(@babel/core@7.28.5) + jest-util: 30.2.0 + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.103.0): dependencies: chalk: 4.1.2 diff --git a/common/scripts/package.json b/common/scripts/package.json index e358cfa469..14f07f204e 100644 --- a/common/scripts/package.json +++ b/common/scripts/package.json @@ -7,8 +7,8 @@ }, "template": "none", "devDependencies": { - "@hcengineering/platform-rig": "^0.7.19", - "esbuild": "^0.25.9", + "@hcengineering/platform-rig": "workspace:^0.7.19", + "esbuild": "^0.25.10", "sharp": "~0.34.3", "@typescript-eslint/eslint-plugin": "^6.21.0", "eslint-plugin-import": "^2.26.0", diff --git a/desktop-package/package.json b/desktop-package/package.json index ce2a0c1ecd..3868f8073c 100644 --- a/desktop-package/package.json +++ b/desktop-package/package.json @@ -15,14 +15,14 @@ "bump": "bump-package-version" }, "devDependencies": { - "@hcengineering/platform-rig": "^0.7.19", + "@hcengineering/platform-rig": "workspace:^0.7.19", "@hcengineering/desktop": "workspace:^0.7.0", "@vercel/webpack-asset-relocator-loader": "^1.7.3", "node-loader": "~2.0.0", "cross-env": "~7.0.3", "typescript": "^5.9.3", "electron": "^38.2.2", - "@types/node": "^22.15.29", + "@types/node": "^22.18.1", "electron-builder": "^25.1.8", "@electron/notarize": "^2.3.2", "@typescript-eslint/eslint-plugin": "^6.21.0", diff --git a/desktop/package.json b/desktop/package.json index e4983ef8dc..5717881e54 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -21,7 +21,7 @@ "bump": "bump-package-version" }, "devDependencies": { - "@hcengineering/platform-rig": "^0.7.19", + "@hcengineering/platform-rig": "workspace:^0.7.19", "@vercel/webpack-asset-relocator-loader": "^1.7.3", "node-loader": "~2.0.0", "cross-env": "~7.0.3", @@ -50,10 +50,10 @@ "update-browserslist-db": "^1.1.3", "browserslist": "^4.25.0", "typescript": "^5.9.3", - "ts-node": "^10.8.0", + "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "electron": "^38.2.2", - "@types/node": "^22.15.29", + "@types/node": "^22.18.1", "copy-webpack-plugin": "^11.0.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", @@ -63,9 +63,9 @@ "eslint-plugin-promise": "^6.1.1", "eslint": "^8.54.0", "prettier": "^3.6.2", - "esbuild": "^0.25.9", + "esbuild": "^0.25.10", "esbuild-loader": "^4.3.0", - "@types/ws": "^8.5.11", + "@types/ws": "^8.5.12", "jest": "^29.7.0", "ts-jest": "^29.1.1", "@types/jest": "^29.5.5", @@ -73,7 +73,7 @@ "jest-environment-jsdom": "^30.2.0" }, "dependencies": { - "@hcengineering/platform": "^0.7.18", + "@hcengineering/platform": "workspace:^0.7.18", "@hcengineering/ui": "workspace:^0.7.0", "@hcengineering/theme": "workspace:^0.7.0", "@hcengineering/login": "workspace:^0.7.0", @@ -82,7 +82,7 @@ "@hcengineering/onboard": "workspace:^0.7.0", "@hcengineering/onboard-assets": "workspace:^0.7.0", "@hcengineering/onboard-resources": "workspace:^0.7.0", - "@hcengineering/client": "^0.7.17", + "@hcengineering/client": "workspace:^0.7.17", "@hcengineering/workbench": "workspace:^0.7.0", "@hcengineering/workbench-resources": "workspace:^0.7.0", "@hcengineering/view": "workspace:^0.7.0", @@ -102,7 +102,7 @@ "@hcengineering/setting": "workspace:^0.7.0", "@hcengineering/setting-assets": "workspace:^0.7.0", "@hcengineering/setting-resources": "workspace:^0.7.0", - "@hcengineering/client-resources": "^0.7.17", + "@hcengineering/client-resources": "workspace:^0.7.17", "@hcengineering/contact-assets": "workspace:^0.7.0", "@hcengineering/activity": "workspace:^0.7.0", "@hcengineering/activity-assets": "workspace:^0.7.0", @@ -133,7 +133,7 @@ "@hcengineering/notification-resources": "workspace:^0.7.0", "@hcengineering/preference": "workspace:^0.7.0", "@hcengineering/preference-assets": "workspace:^0.7.0", - "@hcengineering/core": "^0.7.22", + "@hcengineering/core": "workspace:^0.7.22", "@hcengineering/rekoni": "workspace:^0.7.0", "@hcengineering/tags-assets": "workspace:^0.7.0", "@hcengineering/tags": "workspace:^0.7.0", @@ -277,13 +277,13 @@ "electron-context-menu": "^4.0.4", "electron-windows-badge": "^1.1.0", "svelte": "^4.2.20", - "commander": "^8.1.0", + "commander": "^14.0.0", "electron-store": "^8.2.0", "electron-log": "^5.1.7", "electron-updater": "^6.3.4", "livekit-client": "^2.15.6", "@hcengineering/server-backup": "workspace:^0.7.0", - "@hcengineering/communication-types": "^0.7.12", + "@hcengineering/communication-types": "workspace:^0.7.12", "ws": "^8.18.2" }, "productName": "Huly Desktop", diff --git a/dev/doc-import-tool/package.json b/dev/doc-import-tool/package.json index 4cb7d25849..eed742b67c 100644 --- a/dev/doc-import-tool/package.json +++ b/dev/doc-import-tool/package.json @@ -22,16 +22,16 @@ }, "devDependencies": { "cross-env": "~7.0.3", - "@hcengineering/platform-rig": "^0.7.19", + "@hcengineering/platform-rig": "workspace:^0.7.19", "@typescript-eslint/eslint-plugin": "^6.21.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-n": "^15.4.0", "eslint": "^8.54.0", - "ts-node": "^10.8.0", - "esbuild": "^0.25.9", + "ts-node": "^10.9.2", + "esbuild": "^0.25.10", "@types/minio": "~7.0.11", - "@types/node": "^22.15.29", + "@types/node": "^22.18.1", "@typescript-eslint/parser": "^6.21.0", "eslint-config-standard-with-typescript": "^40.0.0", "prettier": "^3.6.2", @@ -47,15 +47,15 @@ "@hcengineering/account": "workspace:^0.7.0", "@hcengineering/attachment": "workspace:^0.7.0", "@hcengineering/contact": "workspace:^0.7.0", - "@hcengineering/core": "^0.7.22", - "@hcengineering/platform": "^0.7.18", - "@hcengineering/server-core": "^0.7.17", - "@hcengineering/server-storage": "^0.7.16", - "@hcengineering/server-token": "^0.7.17", + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/platform": "workspace:^0.7.18", + "@hcengineering/server-core": "workspace:^0.7.17", + "@hcengineering/server-storage": "workspace:^0.7.16", + "@hcengineering/server-token": "workspace:^0.7.17", "@hcengineering/server-tool": "workspace:^0.7.0", - "@hcengineering/server-client": "^0.7.16", - "@hcengineering/collaborator-client": "^0.7.17", - "commander": "^8.1.0", + "@hcengineering/server-client": "workspace:^0.7.16", + "@hcengineering/collaborator-client": "workspace:^0.7.17", + "commander": "^14.0.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "htmlparser2": "^9.0.0", diff --git a/dev/import-tool/package.json b/dev/import-tool/package.json index 23c2c398df..ee1751b4ae 100644 --- a/dev/import-tool/package.json +++ b/dev/import-tool/package.json @@ -32,12 +32,12 @@ }, "devDependencies": { "cross-env": "~7.0.3", - "@hcengineering/platform-rig": "^0.7.19", + "@hcengineering/platform-rig": "workspace:^0.7.19", "@typescript-eslint/eslint-plugin": "^6.21.0", "eslint": "^8.54.0", - "ts-node": "^10.8.0", - "esbuild": "^0.25.9", - "@types/node": "^22.15.29", + "ts-node": "^10.9.2", + "esbuild": "^0.25.10", + "@types/node": "^22.18.1", "@typescript-eslint/parser": "^6.21.0", "typescript": "^5.9.3", "jest": "^29.7.0", @@ -51,11 +51,11 @@ "@types/js-yaml": "^4.0.9" }, "dependencies": { - "@hcengineering/core": "^0.7.22", - "@hcengineering/platform": "^0.7.18", - "@hcengineering/server-client": "^0.7.16", + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/platform": "workspace:^0.7.18", + "@hcengineering/server-client": "workspace:^0.7.16", "@hcengineering/importer": "workspace:^0.7.0", - "commander": "^8.1.0", + "commander": "^14.0.0", "js-yaml": "^4.1.0", "mammoth": "^1.9.0" } diff --git a/dev/prod/package.json b/dev/prod/package.json index 7308cbff08..871c26927d 100644 --- a/dev/prod/package.json +++ b/dev/prod/package.json @@ -23,15 +23,15 @@ "format": "echo 'no format yet'" }, "devDependencies": { - "@hcengineering/platform-rig": "^0.7.19", - "@types/node": "^22.15.29", + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@types/node": "^22.18.1", "autoprefixer": "^10.4.14", "browserslist": "^4.25.0", "compression-webpack-plugin": "^10.0.0", "cross-env": "~7.0.3", "css-loader": "^5.2.1", "dotenv-webpack": "^8.0.1", - "esbuild": "^0.25.9", + "esbuild": "^0.25.10", "esbuild-loader": "^4.3.0", "file-loader": "^6.2.0", "fork-ts-checker-webpack-plugin": "^9.0.2", @@ -63,7 +63,7 @@ "@hcengineering/activity-resources": "workspace:^0.7.0", "@hcengineering/ai-bot": "workspace:^0.7.0", "@hcengineering/ai-bot-resources": "workspace:^0.7.0", - "@hcengineering/analytics": "^0.7.17", + "@hcengineering/analytics": "workspace:^0.7.17", "@hcengineering/analytics-providers": "workspace:^0.7.0", "@hcengineering/analytics-collector": "workspace:^0.7.0", "@hcengineering/analytics-collector-assets": "workspace:^0.7.0", @@ -89,15 +89,15 @@ "@hcengineering/chunter": "workspace:^0.7.0", "@hcengineering/chunter-assets": "workspace:^0.7.0", "@hcengineering/chunter-resources": "workspace:^0.7.0", - "@hcengineering/client": "^0.7.17", - "@hcengineering/client-resources": "^0.7.17", + "@hcengineering/client": "workspace:^0.7.17", + "@hcengineering/client-resources": "workspace:^0.7.17", "@hcengineering/contact": "workspace:^0.7.0", "@hcengineering/contact-assets": "workspace:^0.7.0", "@hcengineering/contact-resources": "workspace:^0.7.0", "@hcengineering/controlled-documents": "workspace:^0.7.0", "@hcengineering/controlled-documents-assets": "workspace:^0.7.0", "@hcengineering/controlled-documents-resources": "workspace:^0.7.0", - "@hcengineering/core": "^0.7.22", + "@hcengineering/core": "workspace:^0.7.22", "@hcengineering/desktop-preferences": "workspace:^0.7.0", "@hcengineering/desktop-preferences-assets": "workspace:^0.7.0", "@hcengineering/desktop-preferences-resources": "workspace:^0.7.0", @@ -152,7 +152,7 @@ "@hcengineering/onboard": "workspace:^0.7.0", "@hcengineering/onboard-assets": "workspace:^0.7.0", "@hcengineering/onboard-resources": "workspace:^0.7.0", - "@hcengineering/platform": "^0.7.18", + "@hcengineering/platform": "workspace:^0.7.18", "@hcengineering/preference": "workspace:^0.7.0", "@hcengineering/preference-assets": "workspace:^0.7.0", "@hcengineering/presence": "workspace:^0.7.0", diff --git a/dev/storybook/package.json b/dev/storybook/package.json index b0a237e35e..877a7bd7e5 100644 --- a/dev/storybook/package.json +++ b/dev/storybook/package.json @@ -11,9 +11,9 @@ "test": "echo \"No test specified\"" }, "devDependencies": { - "@hcengineering/platform": "^0.7.17", - "@hcengineering/theme": "^0.7.0", - "@hcengineering/ui": "^0.7.0", + "@hcengineering/platform": "workspace:^0.7.18", + "@hcengineering/theme": "workspace:^0.7.0", + "@hcengineering/ui": "workspace:^0.7.0", "@storybook/addon-essentials": "^7.0.6", "@storybook/addon-interactions": "^7.0.6", "@storybook/addon-links": "^7.0.6", diff --git a/dev/tool/package.json b/dev/tool/package.json index bd514aa221..e1e7cf2b7f 100644 --- a/dev/tool/package.json +++ b/dev/tool/package.json @@ -36,21 +36,21 @@ }, "devDependencies": { "cross-env": "~7.0.3", - "@hcengineering/platform-rig": "^0.7.19", + "@hcengineering/platform-rig": "workspace:^0.7.19", "@typescript-eslint/eslint-plugin": "^6.21.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-n": "^15.4.0", "eslint": "^8.54.0", - "ts-node": "^10.8.0", - "esbuild": "^0.25.9", + "ts-node": "^10.9.2", + "esbuild": "^0.25.10", "@types/minio": "~7.0.11", - "@types/node": "^22.15.29", + "@types/node": "^22.18.1", "@typescript-eslint/parser": "^6.21.0", "eslint-config-standard-with-typescript": "^40.0.0", "prettier": "^3.6.2", "typescript": "^5.9.3", - "@types/ws": "^8.5.11", + "@types/ws": "^8.5.12", "@types/mime-types": "~2.1.1", "@types/request": "~2.48.8", "jest": "^29.7.0", @@ -64,24 +64,24 @@ "@hcengineering/communication": "workspace:^0.7.0", "@hcengineering/chat": "workspace:^0.7.0", "@hcengineering/card": "workspace:^0.7.0", - "@hcengineering/rank": "^0.7.17", - "@hcengineering/text-markdown": "^0.7.19", + "@hcengineering/rank": "workspace:^0.7.17", + "@hcengineering/text-markdown": "workspace:^0.7.20", "@hcengineering/account-service": "workspace:^0.7.0", "@hcengineering/workspace-service": "workspace:^0.7.0", "@hcengineering/attachment": "workspace:^0.7.0", "@hcengineering/calendar": "workspace:^0.7.0", "@hcengineering/chunter": "workspace:^0.7.0", - "@hcengineering/client": "^0.7.17", + "@hcengineering/client": "workspace:^0.7.17", "@hcengineering/activity": "workspace:^0.7.0", - "@hcengineering/client-resources": "^0.7.17", + "@hcengineering/client-resources": "workspace:^0.7.17", "@hcengineering/contact": "workspace:^0.7.0", - "@hcengineering/core": "^0.7.22", + "@hcengineering/core": "workspace:^0.7.22", "@hcengineering/controlled-documents": "workspace:^0.7.0", "@hcengineering/document": "workspace:^0.7.0", - "@hcengineering/elastic": "^0.7.16", + "@hcengineering/elastic": "workspace:^0.7.16", "@hcengineering/lead": "workspace:^0.7.0", - "@hcengineering/minio": "^0.7.16", - "@hcengineering/model": "^0.7.17", + "@hcengineering/minio": "workspace:^0.7.16", + "@hcengineering/model": "workspace:^0.7.17", "@hcengineering/model-all": "workspace:^0.7.0", "@hcengineering/model-attachment": "workspace:^0.7.0", "@hcengineering/model-contact": "workspace:^0.7.0", @@ -94,10 +94,10 @@ "@hcengineering/model-task": "workspace:^0.7.0", "@hcengineering/model-activity": "workspace:^0.7.0", "@hcengineering/model-lead": "workspace:^0.7.0", - "@hcengineering/postgres": "^0.7.19", - "@hcengineering/account-client": "^0.7.19", - "@hcengineering/mongo": "^0.7.16", - "@hcengineering/platform": "^0.7.18", + "@hcengineering/postgres": "workspace:^0.7.19", + "@hcengineering/account-client": "workspace:^0.7.19", + "@hcengineering/mongo": "workspace:^0.7.16", + "@hcengineering/platform": "workspace:^0.7.18", "@hcengineering/recruit": "workspace:^0.7.0", "@hcengineering/rekoni": "workspace:^0.7.0", "@hcengineering/server-pipeline": "workspace:^0.7.0", @@ -107,7 +107,7 @@ "@hcengineering/server-collaboration-resources": "workspace:^0.7.0", "@hcengineering/server-backup": "workspace:^0.7.0", "@hcengineering/backup-service": "workspace:^0.7.0", - "@hcengineering/server-storage": "^0.7.16", + "@hcengineering/server-storage": "workspace:^0.7.16", "@hcengineering/server-calendar": "workspace:^0.7.0", "@hcengineering/server-calendar-resources": "workspace:^0.7.0", "@hcengineering/server-card": "workspace:^0.7.0", @@ -116,7 +116,7 @@ "@hcengineering/server-chunter-resources": "workspace:^0.7.0", "@hcengineering/server-contact": "workspace:^0.7.0", "@hcengineering/server-contact-resources": "workspace:^0.7.0", - "@hcengineering/server-core": "^0.7.17", + "@hcengineering/server-core": "workspace:^0.7.17", "@hcengineering/server-document": "workspace:^0.7.0", "@hcengineering/server-document-resources": "workspace:^0.7.0", "@hcengineering/server-drive": "workspace:^0.7.0", @@ -143,9 +143,9 @@ "@hcengineering/server-task-resources": "workspace:^0.7.0", "@hcengineering/server-telegram": "workspace:^0.7.0", "@hcengineering/server-telegram-resources": "workspace:^0.7.0", - "@hcengineering/server-token": "^0.7.17", + "@hcengineering/server-token": "workspace:^0.7.17", "@hcengineering/server-tool": "workspace:^0.7.0", - "@hcengineering/server-client": "^0.7.16", + "@hcengineering/server-client": "workspace:^0.7.16", "@hcengineering/server-tracker": "workspace:^0.7.0", "@hcengineering/server-tracker-resources": "workspace:^0.7.0", "@hcengineering/server-view": "workspace:^0.7.0", @@ -161,17 +161,17 @@ "@hcengineering/setting": "workspace:^0.7.0", "@hcengineering/tags": "workspace:^0.7.0", "@hcengineering/task": "workspace:^0.7.0", - "@hcengineering/text": "^0.7.18", - "@hcengineering/text-core": "^0.7.18", - "@hcengineering/text-ydoc": "^0.7.18", + "@hcengineering/text": "workspace:^0.7.18", + "@hcengineering/text-core": "workspace:^0.7.18", + "@hcengineering/text-ydoc": "workspace:^0.7.18", "@hcengineering/telegram": "workspace:^0.7.0", "@hcengineering/tracker": "workspace:^0.7.0", - "@hcengineering/collaboration": "^0.7.16", - "@hcengineering/datalake": "^0.7.16", - "@hcengineering/retry": "^0.7.17", - "@hcengineering/s3": "^0.7.16", + "@hcengineering/collaboration": "workspace:^0.7.16", + "@hcengineering/datalake": "workspace:^0.7.16", + "@hcengineering/retry": "workspace:^0.7.17", + "@hcengineering/s3": "workspace:^0.7.16", "@hcengineering/kvs-client": "workspace:^0.7.0", - "commander": "^8.1.0", + "commander": "^14.0.0", "csv-parse": "~5.1.0", "email-addresses": "^5.0.0", "fast-equals": "^5.2.2", @@ -184,11 +184,11 @@ "utf-8-validate": "^6.0.4", "msgpackr": "^1.11.2", "msgpackr-extract": "^3.0.3", - "@hcengineering/kafka": "^0.7.18", - "@hcengineering/api-client": "^0.7.18", + "@hcengineering/kafka": "workspace:^0.7.18", + "@hcengineering/api-client": "workspace:^0.7.18", "@faker-js/faker": "^8.4.1", - "@hcengineering/hulylake-client": "^0.7.17", - "@hcengineering/communication-types": "^0.7.12", + "@hcengineering/hulylake-client": "workspace:^0.7.17", + "@hcengineering/communication-types": "workspace:^0.7.12", "@hcengineering/pod-rating": "workspace:^0.7.0" } } diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 4b0ca60591..56acb52fea 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -933,15 +933,15 @@ export function devTool ( .option('-s, --skip ', 'A list of ; separated domain names to skip during backup', '') .option('--full', 'Full recheck', false) .option( - '-ct, --contentTypes ', + '--ct, --contentTypes ', 'A list of ; separated content types for blobs to skip download if size >= limit', '' ) - .option('-bl, --blobLimit ', 'A blob size limit in megabytes (default 5mb)', '5') + .option('--bl, --blobLimit ', 'A blob size limit in megabytes (default 5mb)', '5') .option('-f, --force', 'Force backup', false) .option('-t, --timeout ', 'Connect timeout in seconds', '30') .option('-k, --keepSnapshots ', 'Keep snapshots for days', '14') - .option('-fv, --fullVerify', 'Full verification', false) + .option('--fv, --fullVerify', 'Full verification', false) .action( async ( dirName: string, @@ -1035,7 +1035,7 @@ export function devTool ( .description('Compact a given backup, will create one snapshot clean unused resources') .option('-f, --force', 'Force compact.', false) .option( - '-ct, --contentTypes ', + '--ct, --contentTypes ', 'A list of ; separated content types for blobs to exclude from backup', 'video/;application/octet-stream;audio/;image/' ) @@ -1312,7 +1312,7 @@ export function devTool ( .description('Compact a given backup to just one snapshot') .option('-f, --force', 'Force compact.', false) .option( - '-ct, --contentTypes ', + '--ct, --contentTypes ', 'A list of ; separated content types for blobs to exclude from backup', 'video/;application/octet-stream;audio/;image/' ) @@ -2666,7 +2666,7 @@ export function devTool ( program .command('queue-init-topics') .description('create required kafka topics') - .option('-tx ', 'Number of TX partitions', '5') + .option('--tx ', 'Number of TX partitions', '5') .action(async (cmd: { tx: string }) => { const queue = getPlatformQueue('tool') await queue.createTopics(parseInt(cmd.tx ?? '1')) diff --git a/foundations/communication/.gitattributes b/foundations/communication/.gitattributes new file mode 100644 index 0000000000..79a85db5c2 --- /dev/null +++ b/foundations/communication/.gitattributes @@ -0,0 +1,14 @@ +# Don't allow people to merge changes to these generated files, because the result +# may be invalid. You need to run "rush update" again. +pnpm-lock.yaml merge=text +shrinkwrap.yaml merge=binary +npm-shrinkwrap.json merge=binary +yarn.lock merge=binary + +# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic +# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor +# may also require a special configuration to allow comments in JSON. +# +# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 +# +*.json linguist-language=JSON-with-Comments diff --git a/foundations/communication/.github/workflows/ci.yml b/foundations/communication/.github/workflows/ci.yml new file mode 100644 index 0000000000..63bb264241 --- /dev/null +++ b/foundations/communication/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: ['main'] + tags: + - 'v0.7.*' + - 's0.7.*' + pull_request: + branches: ['main'] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + - uses: actions/setup-node@v3 + with: + node-version: 22 + - name: Verify Change Logs + run: node common/scripts/install-run-rush.js change --verify + - name: Rush Install + run: node common/scripts/install-run-rush.js install + - name: Rush validate + run: node common/scripts/install-run-rush.js validate --verbose + - name: Rush test + run: node common/scripts/install-run-rush.js test --verbose + - name: Publish packages + if: startsWith(github.ref, 'refs/tags/v0.7.') || startsWith(github.ref, 'refs/tags/s0.7.') + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: node common/scripts/install-run-rush.js publish --include-all --publish diff --git a/foundations/communication/.gitignore b/foundations/communication/.gitignore new file mode 100644 index 0000000000..97b0f884d9 --- /dev/null +++ b/foundations/communication/.gitignore @@ -0,0 +1,140 @@ +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +rush-logs/ + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov/ + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output/ + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt/ + +# Bower dependency directory (https://bower.io/) +bower_components/ + +# node-waf configuration +.lock-wscript/ + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release/ + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm/ + +# Optional eslint cache +.eslintcache/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# next.js build output +.next/ + +# Docusaurus cache and generated files +.docusaurus/ + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# yarn v2 +.yarn/cache/ +.yarn/unplugged/ +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# OS X temporary files +.DS_Store + +# IntelliJ IDEA project files; if you want to commit IntelliJ settings, this recipe may be helpful: +# https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +.idea/ +*.iml + +# Visual Studio Code +.vscode/ +!.vscode/tasks.json +!.vscode/launch.json + +# Rush temporary files +common/deploy/ +common/temp/ +common/autoinstallers/*/.npmrc +**/.rush/temp/ +*.lock + +# Common toolchain intermediate files +temp/ +lib/ +lib-amd/ +lib-es6/ +lib-esnext/ +lib-commonjs/ +lib-shim/ +dist/ +dist-storybook/ + +# Heft temporary files +.cache/ +.heft/ +.rush/ +.validate + +# build output +.turbo/ +**/types/ + +!packages/types/ + +# VS Code settings +.vscode/settings.json + +# logs +pnpm-debug.log* + +# environment variables +.env.production + diff --git a/foundations/communication/.npmrc b/foundations/communication/.npmrc new file mode 100644 index 0000000000..e69de29bb2 diff --git a/foundations/communication/.prettierrc b/foundations/communication/.prettierrc new file mode 100644 index 0000000000..d0f0f53741 --- /dev/null +++ b/foundations/communication/.prettierrc @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "trailingComma": "none", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 120, + "useTabs": false, + "bracketSpacing": true, + "proseWrap": "preserve" +} \ No newline at end of file diff --git a/foundations/communication/.version b/foundations/communication/.version new file mode 100644 index 0000000000..9868d567ed --- /dev/null +++ b/foundations/communication/.version @@ -0,0 +1 @@ +0.1.195 diff --git a/foundations/communication/README.md b/foundations/communication/README.md new file mode 100644 index 0000000000..a046e7bb0e --- /dev/null +++ b/foundations/communication/README.md @@ -0,0 +1,10 @@ +# Important Notice + +**This repository is not a standalone project** and is intended to be used **only** as a submodule within https://github.com/hcengineering/platform. + +There is no separate CI, release pipeline, or independent build in this directory. +All install and build commands must be run from the root of the main repository (for example: rush install && rush build). +Changes in this folder will not take effect unless the parent repository initializes and updates this submodule. + + +Cloning this repository by itself will not provide the full dependency structure. To set everything up correctly, clone and initialize https://github.com/hcengineering/platform. diff --git a/foundations/communication/common/changes/@hcengineering/communication-rest-client/main_2025-10-29-08-20.json b/foundations/communication/common/changes/@hcengineering/communication-rest-client/main_2025-10-29-08-20.json new file mode 100644 index 0000000000..6ef798c1dc --- /dev/null +++ b/foundations/communication/common/changes/@hcengineering/communication-rest-client/main_2025-10-29-08-20.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@hcengineering/communication-rest-client", + "comment": "update deps", + "type": "patch" + } + ], + "packageName": "@hcengineering/communication-rest-client" +} \ No newline at end of file diff --git a/foundations/communication/common/changes/@hcengineering/communication-sdk-types/main_2025-10-29-08-20.json b/foundations/communication/common/changes/@hcengineering/communication-sdk-types/main_2025-10-29-08-20.json new file mode 100644 index 0000000000..4fe5123b5c --- /dev/null +++ b/foundations/communication/common/changes/@hcengineering/communication-sdk-types/main_2025-10-29-08-20.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@hcengineering/communication-sdk-types", + "comment": "update deps", + "type": "patch" + } + ], + "packageName": "@hcengineering/communication-sdk-types" +} \ No newline at end of file diff --git a/foundations/communication/common/changes/@hcengineering/communication-server/main_2025-10-29-08-20.json b/foundations/communication/common/changes/@hcengineering/communication-server/main_2025-10-29-08-20.json new file mode 100644 index 0000000000..7a62a00716 --- /dev/null +++ b/foundations/communication/common/changes/@hcengineering/communication-server/main_2025-10-29-08-20.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@hcengineering/communication-server", + "comment": "update deps", + "type": "patch" + } + ], + "packageName": "@hcengineering/communication-server" +} \ No newline at end of file diff --git a/foundations/communication/common/changes/@hcengineering/communication-types/main_2025-10-29-08-20.json b/foundations/communication/common/changes/@hcengineering/communication-types/main_2025-10-29-08-20.json new file mode 100644 index 0000000000..dbb851e6cb --- /dev/null +++ b/foundations/communication/common/changes/@hcengineering/communication-types/main_2025-10-29-08-20.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@hcengineering/communication-types", + "comment": "update deps", + "type": "patch" + } + ], + "packageName": "@hcengineering/communication-types" +} \ No newline at end of file diff --git a/foundations/communication/common/config/rush/.npmrc b/foundations/communication/common/config/rush/.npmrc new file mode 100644 index 0000000000..4bb3c57a4e --- /dev/null +++ b/foundations/communication/common/config/rush/.npmrc @@ -0,0 +1,33 @@ +# Rush uses this file to configure the NPM package registry during installation. It is applicable +# to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", +# "rush update", and the "install-run.js" scripts. +# +# NOTE: The "rush publish" command uses .npmrc-publish instead. +# +# Before invoking the package manager, Rush will generate an .npmrc in the folder where installation +# is performed. This generated file will omit any config lines that reference environment variables +# that are undefined in that session; this avoids problems that would otherwise result due to +# a missing variable being replaced by an empty string. +# +# If "subspacesEnabled" is true in subspaces.json, the generated file will merge settings from +# "common/config/rush/.npmrc" and "common/config/subspaces//.npmrc", with the latter taking +# precedence. +# +# * * * SECURITY WARNING * * * +# +# It is NOT recommended to store authentication tokens in a text file on a lab machine, because +# other unrelated processes may be able to read that file. Also, the file may persist indefinitely, +# for example if the machine loses power. A safer practice is to pass the token via an +# environment variable, which can be referenced from .npmrc using ${} expansion. For example: +# +# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} +# + +# Explicitly specify the NPM registry that "rush install" and "rush update" will use by default: +registry=https://registry.npmjs.org/ + +# Optionally provide an authentication token for the above registry URL (if it is a private registry): +//registry.npmjs.org/:_authToken=${NPM_TOKEN} + +# Change this to "true" if your registry requires authentication for read-only operations: +always-auth=false diff --git a/foundations/communication/common/config/rush/.npmrc-publish b/foundations/communication/common/config/rush/.npmrc-publish new file mode 100644 index 0000000000..951a4918cf --- /dev/null +++ b/foundations/communication/common/config/rush/.npmrc-publish @@ -0,0 +1,29 @@ +# This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish +# is used by the "rush publish" command, as publishing often involves different credentials +# and registries than other operations. +# +# Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc" +# and then temporarily map that folder as the "home directory" for the current user account. +# This enables the same settings to apply for each project folder that gets published. The copied file +# will omit any config lines that reference environment variables that are undefined in that session; +# this avoids problems that would otherwise result due to a missing variable being replaced by +# an empty string. +# +# * * * SECURITY WARNING * * * +# +# It is NOT recommended to store authentication tokens in a text file on a lab machine, because +# other unrelated processes may be able to read the file. Also, the file may persist indefinitely, +# for example if the machine loses power. A safer practice is to pass the token via an +# environment variable, which can be referenced from .npmrc using ${} expansion. For example: +# +# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} +# + +# Explicitly specify the NPM registry that "rush publish" will use by default: +registry=https://registry.npmjs.org/ + +# Optionally provide an authentication token for the above registry URL (if it is a private registry): +//registry.npmjs.org/:_authToken=${NPM_TOKEN} + +# Change this to "true" if your registry requires authentication for read-only operations: +always-auth=false diff --git a/foundations/communication/common/config/rush/.pnpmfile.cjs b/foundations/communication/common/config/rush/.pnpmfile.cjs new file mode 100644 index 0000000000..98cf3279ec --- /dev/null +++ b/foundations/communication/common/config/rush/.pnpmfile.cjs @@ -0,0 +1,38 @@ +'use strict'; + +/** + * When using the PNPM package manager, you can use pnpmfile.js to workaround + * dependencies that have mistakes in their package.json file. (This feature is + * functionally similar to Yarn's "resolutions".) + * + * For details, see the PNPM documentation: + * https://pnpm.io/pnpmfile#hooks + * + * IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY TO INVALIDATE + * ANY CACHED DEPENDENCY ANALYSIS. After any modification to pnpmfile.js, it's recommended to run + * "rush update --full" so that PNPM will recalculate all version selections. + */ +module.exports = { + hooks: { + readPackage + } +}; + +/** + * This hook is invoked during installation before a package's dependencies + * are selected. + * The `packageJson` parameter is the deserialized package.json + * contents for the package that is about to be installed. + * The `context` parameter provides a log() function. + * The return value is the updated object. + */ +function readPackage(packageJson, context) { + + // // The karma types have a missing dependency on typings from the log4js package. + // if (packageJson.name === '@types/karma') { + // context.log('Fixed up dependencies for @types/karma'); + // packageJson.dependencies['log4js'] = '0.6.38'; + // } + + return packageJson; +} diff --git a/foundations/communication/common/config/rush/artifactory.json b/foundations/communication/common/config/rush/artifactory.json new file mode 100644 index 0000000000..268065478a --- /dev/null +++ b/foundations/communication/common/config/rush/artifactory.json @@ -0,0 +1,109 @@ +/** + * This configuration file manages Rush integration with JFrog Artifactory services. + * More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/artifactory.schema.json", + + "packageRegistry": { + /** + * (Required) Set this to "true" to enable Rush to manage tokens for an Artifactory NPM registry. + * When enabled, "rush install" will automatically detect when the user's ~/.npmrc + * authentication token is missing or expired. And "rush setup" will prompt the user to + * renew their token. + * + * The default value is false. + */ + "enabled": false, + + /** + * (Required) Specify the URL of your NPM registry. This is the same URL that appears in + * your .npmrc file. It should look something like this example: + * + * https://your-company.jfrog.io/your-project/api/npm/npm-private/ + */ + "registryUrl": "", + + /** + * A list of custom strings that "rush setup" should add to the user's ~/.npmrc file at the time + * when the token is updated. This could be used for example to configure the company registry + * to be used whenever NPM is invoked as a standalone command (but it's not needed for Rush + * operations like "rush add" and "rush install", which get their mappings from the monorepo's + * common/config/rush/.npmrc file). + * + * NOTE: The ~/.npmrc settings are global for the user account on a given machine, so be careful + * about adding settings that may interfere with other work outside the monorepo. + */ + "userNpmrcLinesToAdd": [ + // "@example:registry=https://your-company.jfrog.io/your-project/api/npm/npm-private/" + ], + + /** + * (Required) Specifies the URL of the Artifactory control panel where the user can generate + * an API key. This URL is printed after the "visitWebsite" message. + * It should look something like this example: https://your-company.jfrog.io/ + * Specify an empty string to suppress this line entirely. + */ + "artifactoryWebsiteUrl": "", + + /** + * Uncomment this line to specify the type of credential to save in the user's ~/.npmrc file. + * The default is "password", which means the user's API token will be traded in for an + * npm password specific to that registry. Optionally you can specify "authToken", which + * will save the user's API token as credentials instead. + */ + // "credentialType": "password", + + /** + * These settings allow the "rush setup" interactive prompts to be customized, for + * example with messages specific to your team or configuration. Specify an empty string + * to suppress that message entirely. + */ + "messageOverrides": { + /** + * Overrides the message that normally says: + * "This monorepo consumes packages from an Artifactory private NPM registry." + */ + // "introduction": "", + + /** + * Overrides the message that normally says: + * "Please contact the repository maintainers for help with setting up an Artifactory user account." + */ + // "obtainAnAccount": "", + + /** + * Overrides the message that normally says: + * "Please open this URL in your web browser:" + * + * The "artifactoryWebsiteUrl" string is printed after this message. + */ + // "visitWebsite": "", + + /** + * Overrides the message that normally says: + * "Your user name appears in the upper-right corner of the JFrog website." + */ + // "locateUserName": "", + + /** + * Overrides the message that normally says: + * "Click 'Edit Profile' on the JFrog website. Click the 'Generate API Key' + * button if you haven't already done so previously." + */ + // "locateApiKey": "" + + /** + * Overrides the message that normally prompts: + * "What is your Artifactory user name?" + */ + // "userNamePrompt": "" + + /** + * Overrides the message that normally prompts: + * "What is your Artifactory API key?" + */ + // "apiKeyPrompt": "" + } + } +} diff --git a/foundations/communication/common/config/rush/build-cache.json b/foundations/communication/common/config/rush/build-cache.json new file mode 100644 index 0000000000..072e9f7d49 --- /dev/null +++ b/foundations/communication/common/config/rush/build-cache.json @@ -0,0 +1,160 @@ +/** + * This configuration file manages Rush's build cache feature. + * More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json", + + /** + * (Required) EXPERIMENTAL - Set this to true to enable the build cache feature. + * + * See https://rushjs.io/pages/maintainer/build_cache/ for details about this experimental feature. + */ + "buildCacheEnabled": false, + + /** + * (Required) Choose where project build outputs will be cached. + * + * Possible values: "local-only", "azure-blob-storage", "amazon-s3" + */ + "cacheProvider": "local-only", + + /** + * Setting this property overrides the cache entry ID. If this property is set, it must contain + * a [hash] token. + * + * Other available tokens: + * - [projectName] Example: "@my-scope/my-project" + * - [projectName:normalize] Example: "my-scope+my-project" + * - [phaseName] Example: "_phase:test/api" + * - [phaseName:normalize] Example: "_phase:test+api" + * - [phaseName:trimPrefix] Example: "test/api" + * - [os] Example: "win32" + * - [arch] Example: "x64" + */ + // "cacheEntryNamePattern": "[projectName:normalize]-[phaseName:normalize]-[hash]" + + /** + * (Optional) Salt to inject during calculation of the cache key. This can be used to invalidate the cache for all projects when the salt changes. + */ + // "cacheHashSalt": "1", + + /** + * Use this configuration with "cacheProvider"="azure-blob-storage" + */ + "azureBlobStorageConfiguration": { + /** + * (Required) The name of the the Azure storage account to use for build cache. + */ + // "storageAccountName": "example", + + /** + * (Required) The name of the container in the Azure storage account to use for build cache. + */ + // "storageContainerName": "my-container", + + /** + * The Azure environment the storage account exists in. Defaults to AzurePublicCloud. + * + * Possible values: "AzurePublicCloud", "AzureChina", "AzureGermany", "AzureGovernment" + */ + // "azureEnvironment": "AzurePublicCloud", + + /** + * An optional prefix for cache item blob names. + */ + // "blobPrefix": "my-prefix", + + /** + * If set to true, allow writing to the cache. Defaults to false. + */ + // "isCacheWriteAllowed": true, + + /** + * The Entra ID login flow to use. Defaults to 'AdoCodespacesAuth' on GitHub Codespaces, 'InteractiveBrowser' otherwise. + */ + // "loginFlow": "InteractiveBrowser", + + /** + * If set to true, reading the cache requires authentication. Defaults to false. + */ + // "readRequiresAuthentication": true + }, + + /** + * Use this configuration with "cacheProvider"="amazon-s3" + */ + "amazonS3Configuration": { + /** + * (Required unless s3Endpoint is specified) The name of the bucket to use for build cache. + * Example: "my-bucket" + */ + // "s3Bucket": "my-bucket", + + /** + * (Required unless s3Bucket is specified) The Amazon S3 endpoint of the bucket to use for build cache. + * This should not include any path; use the s3Prefix to set the path. + * Examples: "my-bucket.s3.us-east-2.amazonaws.com" or "http://localhost:9000" + */ + // "s3Endpoint": "https://my-bucket.s3.us-east-2.amazonaws.com", + + /** + * (Required) The Amazon S3 region of the bucket to use for build cache. + * Example: "us-east-1" + */ + // "s3Region": "us-east-1", + + /** + * An optional prefix ("folder") for cache items. It should not start with "/". + */ + // "s3Prefix": "my-prefix", + + /** + * If set to true, allow writing to the cache. Defaults to false. + */ + // "isCacheWriteAllowed": true + }, + + /** + * Use this configuration with "cacheProvider"="http" + */ + "httpConfiguration": { + /** + * (Required) The URL of the server that stores the caches. + * Example: "https://build-cacches.example.com/" + */ + // "url": "https://build-cacches.example.com/", + + /** + * (Optional) The HTTP method to use when writing to the cache (defaults to PUT). + * Should be one of PUT, POST, or PATCH. + * Example: "PUT" + */ + // "uploadMethod": "PUT", + + /** + * (Optional) HTTP headers to pass to the cache server. + * Example: { "X-HTTP-Company-Id": "109283" } + */ + // "headers": {}, + + /** + * (Optional) Shell command that prints the authorization token needed to communicate with the + * cache server, and exits with exit code 0. This command will be executed from the root of + * the monorepo. + * Example: { "exec": "node", "args": ["common/scripts/auth.js"] } + */ + // "tokenHandler": { "exec": "node", "args": ["common/scripts/auth.js"] }, + + /** + * (Optional) Prefix for cache keys. + * Example: "my-company-" + */ + // "cacheKeyPrefix": "", + + /** + * (Optional) If set to true, allow writing to the cache. Defaults to false. + */ + // "isCacheWriteAllowed": true + } +} diff --git a/foundations/communication/common/config/rush/cobuild.json b/foundations/communication/common/config/rush/cobuild.json new file mode 100644 index 0000000000..a47fad18d5 --- /dev/null +++ b/foundations/communication/common/config/rush/cobuild.json @@ -0,0 +1,22 @@ +/** + * This configuration file manages Rush's cobuild feature. + * More documentation is available on the Rush website: https://rushjs.io + */ + { + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/cobuild.schema.json", + + /** + * (Required) EXPERIMENTAL - Set this to true to enable the cobuild feature. + * RUSH_COBUILD_CONTEXT_ID should always be specified as an environment variable with an non-empty string, + * otherwise the cobuild feature will be disabled. + */ + "cobuildFeatureEnabled": false, + + /** + * (Required) Choose where cobuild lock will be acquired. + * + * The lock provider is registered by the rush plugins. + * For example, @rushstack/rush-redis-cobuild-plugin registers the "redis" lock provider. + */ + "cobuildLockProvider": "redis" +} diff --git a/foundations/communication/common/config/rush/command-line.json b/foundations/communication/common/config/rush/command-line.json new file mode 100644 index 0000000000..5064b33ef9 --- /dev/null +++ b/foundations/communication/common/config/rush/command-line.json @@ -0,0 +1,262 @@ +/** + * This configuration file defines custom commands for the "rush" command-line. + * More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", + + "phases": [ + { + "name": "_phase:build", + "dependencies": { + "upstream": ["_phase:build"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": false + }, + { + "name": "_phase:validate", + "dependencies": { + "self": ["_phase:build"], + "upstream": ["_phase:validate", "_phase:build"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": false + }, + { + "name": "_phase:bundle", + "dependencies": { + "self": ["_phase:build"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": false + }, + { + "name": "_phase:test", + "dependencies": { + "self": ["_phase:build"], + "upstream": ["_phase:validate"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": true + } + ], + "commands": [ + { + "commandKind": "global", + "name": "coverage", + "summary": "Run tests, merge LCOV and generate HTML coverage", + "description": "Run 'rush test', then merge per-package LCOV files and generate HTML coverage in coverage/html", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "rush test && node scripts/merge-coverage.js && node scripts/generate-coverage-html.js coverage/lcov.info coverage/html" + }, + { + "commandKind": "phased", + "summary": "Do testing", + "name": "test", + "phases": ["_phase:build", "_phase:test"], + "enableParallelism": true, + "incremental": true + }, + { + "commandKind": "phased", + "name": "build", + "summary": "build", + "phases": ["_phase:build"], + "enableParallelism": true, + "incremental": true, + "watchOptions": { + "alwaysWatch": false, + "watchPhases": ["_phase:build"] + } + }, + { + "commandKind": "phased", + "name": "validate", + "phases": ["_phase:validate"], + "summary": "validate", + "enableParallelism": true, + "incremental": true + }, + { + "commandKind": "bulk", + "name": "format", + "summary": "Format", + "description": "Perform a formatting", + "enableParallelism": true, + "incremental": false, + "ignoreMissingScript": true, + "safeForSimultaneousRushProcesses": true, + "disableBuildCache": true + }, + { + "commandKind": "bulk", + "name": "lint", + "summary": "Lint", + "description": "Linting", + "enableParallelism": true, + "incremental": false, + "ignoreMissingScript": true, + "safeForSimultaneousRushProcesses": true, + "disableBuildCache": true + }, + { + "commandKind": "bulk", + "name": "lint:fix", + "summary": "Lint & fix", + "description": "Linting and fixing", + "enableParallelism": true, + "incremental": false, + "ignoreMissingScript": true, + "safeForSimultaneousRushProcesses": true, + "disableBuildCache": true + }, + { + "commandKind": "global", + "name": "bump-changes", + "summary": "Bump changes from tag", + "description": "Bump changes from previous tag", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "./common/scripts/node_modules/.bin/bump-changes-from-tag" + } + ], + /** + * Custom "parameters" introduce new parameters for specified Rush command-line commands. + * For example, you might define a "--production" parameter for the "rush build" command. + */ + "parameters": [ + { + "parameterKind": "flag", + "longName": "--lite", + "shortName": "-l", + "description": "Enable Heft lite building option, will skip some phases.", + "associatedCommands": ["build"] + }, + { + "parameterKind": "flag", + "longName": "--clean", + "description": "Enable Heft clean building option", + "associatedCommands": ["build"] + } + // { + // /** + // * (Required) Determines the type of custom parameter. + // * A "flag" is a custom command-line parameter whose presence acts as an on/off switch. + // */ + // "parameterKind": "flag", + // + // /** + // * (Required) The long name of the parameter. It must be lower-case and use dash delimiters. + // */ + // "longName": "--my-flag", + // + // /** + // * An optional alternative short name for the parameter. It must be a dash followed by a single + // * lower-case or upper-case letter, which is case-sensitive. + // * + // * NOTE: The Rush developers recommend that automation scripts should always use the long name + // * to improve readability. The short name is only intended as a convenience for humans. + // * The alphabet letters run out quickly, and are difficult to memorize, so *only* use + // * a short name if you expect the parameter to be needed very often in everyday operations. + // */ + // "shortName": "-m", + // + // /** + // * (Required) A long description to be shown in the command-line help. + // * + // * Whenever you introduce commands/parameters, taking a little time to write meaningful + // * documentation can make a big difference for the developer experience in your repo. + // */ + // "description": "A custom flag parameter that is passed to the scripts that are invoked when building projects", + // + // /** + // * (Required) A list of custom commands and/or built-in Rush commands that this parameter may + // * be used with. The parameter will be appended to the shell command that Rush invokes. + // */ + // "associatedCommands": ["build", "rebuild"] + // }, + // + // { + // /** + // * (Required) Determines the type of custom parameter. + // * A "string" is a custom command-line parameter whose value is a simple text string. + // */ + // "parameterKind": "string", + // "longName": "--my-string", + // "description": "A custom string parameter for the \"my-global-command\" custom command", + // + // "associatedCommands": ["my-global-command"], + // + // /** + // * The name of the argument, which will be shown in the command-line help. + // * + // * For example, if the parameter name is '--count" and the argument name is "NUMBER", + // * then the command-line help would display "--count NUMBER". The argument name must + // * be comprised of upper-case letters, numbers, and underscores. It should be kept short. + // */ + // "argumentName": "SOME_TEXT", + // + // /** + // * If true, this parameter must be included with the command. The default is false. + // */ + // "required": false + // }, + // + // { + // /** + // * (Required) Determines the type of custom parameter. + // * A "choice" is a custom command-line parameter whose argument must be chosen from a list of + // * allowable alternatives. + // */ + // "parameterKind": "choice", + // "longName": "--my-choice", + // "description": "A custom choice parameter for the \"my-global-command\" custom command", + // + // "associatedCommands": ["my-global-command"], + // + // /** + // * If true, this parameter must be included with the command. The default is false. + // */ + // "required": false, + // + // /** + // * Normally if a parameter is omitted from the command line, it will not be passed + // * to the shell command. this value will be inserted by default. Whereas if a "defaultValue" + // * is defined, the parameter will always be passed to the shell command, and will use the + // * default value if unspecified. The value must be one of the defined alternatives. + // */ + // "defaultValue": "vanilla", + // + // /** + // * (Required) A list of alternative argument values that can be chosen for this parameter. + // */ + // "alternatives": [ + // { + // /** + // * A token that is one of the alternatives that can be used with the choice parameter, + // * e.g. "vanilla" in "--flavor vanilla". + // */ + // "name": "vanilla", + // + // /** + // * A detailed description for the alternative that can be shown in the command-line help. + // * + // * Whenever you introduce commands/parameters, taking a little time to write meaningful + // * documentation can make a big difference for the developer experience in your repo. + // */ + // "description": "Use the vanilla flavor (the default)" + // }, + // + // { + // "name": "chocolate", + // "description": "Use the chocolate flavor" + // }, + // + // { + // "name": "strawberry", + // "description": "Use the strawberry flavor" + // } + // ] + // } + ] +} diff --git a/foundations/communication/common/config/rush/common-versions.json b/foundations/communication/common/config/rush/common-versions.json new file mode 100644 index 0000000000..4a3ecbd1ee --- /dev/null +++ b/foundations/communication/common/config/rush/common-versions.json @@ -0,0 +1,77 @@ +/** + * This configuration file specifies NPM dependency version selections that affect all projects + * in a Rush repo. More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", + + /** + * A table that specifies a "preferred version" for a given NPM package. This feature is typically used + * to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies. + * + * The "preferredVersions" value can be any SemVer range specifier (e.g. "~1.2.3"). Rush injects these values into + * the "dependencies" field of the top-level common/temp/package.json, which influences how the package manager + * will calculate versions. The specific effect depends on your package manager. Generally it will have no + * effect on an incompatible or already constrained SemVer range. If you are using PNPM, similar effects can be + * achieved using the pnpmfile.js hook. See the Rush documentation for more details. + * + * After modifying this field, it's recommended to run "rush update --full" so that the package manager + * will recalculate all version selections. + */ + "preferredVersions": { + /** + * When someone asks for "^1.0.0" make sure they get "1.2.3" when working in this repo, + * instead of the latest version. + */ + // "some-library": "1.2.3" + }, + + /** + * When set to true, for all projects in the repo, all dependencies will be automatically added as preferredVersions, + * except in cases where different projects specify different version ranges for a given dependency. For older + * package managers, this tended to reduce duplication of indirect dependencies. However, it can sometimes cause + * trouble for indirect dependencies with incompatible peerDependencies ranges. + * + * The default value is true. If you're encountering installation errors related to peer dependencies, + * it's recommended to set this to false. + * + * After modifying this field, it's recommended to run "rush update --full" so that the package manager + * will recalculate all version selections. + */ + // "implicitlyPreferredVersions": false, + + /** + * If you would like the version specifiers for your dependencies to be consistent, then + * uncomment this line. This is effectively similar to running "rush check" before any + * of the following commands: + * + * rush install, rush update, rush link, rush version, rush publish + * + * In some cases you may want this turned on, but need to allow certain packages to use a different + * version. In those cases, you will need to add an entry to the "allowedAlternativeVersions" + * section of the common-versions.json. + * + * In the case that subspaces is enabled, this setting will take effect at a subspace level. + */ + // "ensureConsistentVersions": true, + + /** + * The "rush check" command can be used to enforce that every project in the repo must specify + * the same SemVer range for a given dependency. However, sometimes exceptions are needed. + * The allowedAlternativeVersions table allows you to list other SemVer ranges that will be + * accepted by "rush check" for a given dependency. + * + * IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE + * USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO). + * This design avoids unnecessary churn in this file. + */ + "allowedAlternativeVersions": { + /** + * For example, allow some projects to use an older TypeScript compiler + * (in addition to whatever "usual" version is being used by other projects in the repo): + */ + // "typescript": [ + // "~2.4.0" + // ] + } +} \ No newline at end of file diff --git a/foundations/communication/common/config/rush/custom-tips.json b/foundations/communication/common/config/rush/custom-tips.json new file mode 100644 index 0000000000..31e540d6ad --- /dev/null +++ b/foundations/communication/common/config/rush/custom-tips.json @@ -0,0 +1,29 @@ +/** + * This configuration file allows repo maintainers to configure extra details to be + * printed alongside certain Rush messages. More documentation is available on the + * Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/custom-tips.schema.json", + + /** + * Custom tips allow you to annotate Rush's console messages with advice tailored for + * your specific monorepo. + */ + "customTips": [ + // { + // /** + // * (REQUIRED) An identifier indicating a message that may be printed by Rush. + // * If that message is printed, then this custom tip will be shown. + // * The list of available tip identifiers can be found on this page: + // * https://rushjs.io/pages/maintainer/custom_tips/ + // */ + // "tipId": "TIP_RUSH_INCONSISTENT_VERSIONS", + // + // /** + // * (REQUIRED) The message text to be displayed for this tip. + // */ + // "message": "For additional troubleshooting information, refer this wiki article:\n\nhttps://intranet.contoso.com/docs/pnpm-mismatch" + // } + ] +} diff --git a/foundations/communication/common/config/rush/experiments.json b/foundations/communication/common/config/rush/experiments.json new file mode 100644 index 0000000000..01f8f8f902 --- /dev/null +++ b/foundations/communication/common/config/rush/experiments.json @@ -0,0 +1,121 @@ +/** + * This configuration file allows repo maintainers to enable and disable experimental + * Rush features. More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json", + + /** + * By default, 'rush install' passes --no-prefer-frozen-lockfile to 'pnpm install'. + * Set this option to true to pass '--frozen-lockfile' instead for faster installs. + */ + // "usePnpmFrozenLockfileForRushInstall": true, + + /** + * By default, 'rush update' passes --no-prefer-frozen-lockfile to 'pnpm install'. + * Set this option to true to pass '--prefer-frozen-lockfile' instead to minimize shrinkwrap changes. + */ + // "usePnpmPreferFrozenLockfileForRushUpdate": true, + + /** + * By default, 'rush update' runs as a single operation. + * Set this option to true to instead update the lockfile with `--lockfile-only`, then perform a `--frozen-lockfile` install. + * Necessary when using the `afterAllResolved` hook in .pnpmfile.cjs. + */ + // "usePnpmLockfileOnlyThenFrozenLockfileForRushUpdate": true, + + /** + * If using the 'preventManualShrinkwrapChanges' option, restricts the hash to only include the layout of external dependencies. + * Used to allow links between workspace projects or the addition/removal of references to existing dependency versions to not + * cause hash changes. + */ + // "omitImportersFromPreventManualShrinkwrapChanges": true, + + /** + * If true, the chmod field in temporary project tar headers will not be normalized. + * This normalization can help ensure consistent tarball integrity across platforms. + */ + // "noChmodFieldInTarHeaderNormalization": true, + + /** + * If true, build caching will respect the allowWarningsInSuccessfulBuild flag and cache builds with warnings. + * This will not replay warnings from the cached build. + */ + // "buildCacheWithAllowWarningsInSuccessfulBuild": true, + + /** + * If true, build skipping will respect the allowWarningsInSuccessfulBuild flag and skip builds with warnings. + * This will not replay warnings from the skipped build. + */ + // "buildSkipWithAllowWarningsInSuccessfulBuild": true, + + /** + * If true, perform a clean install after when running `rush install` or `rush update` if the + * `.npmrc` file has changed since the last install. + */ + // "cleanInstallAfterNpmrcChanges": true, + + /** + * If true, print the outputs of shell commands defined in event hooks to the console. + */ + // "printEventHooksOutputToConsole": true, + + /** + * If true, Rush will not allow node_modules in the repo folder or in parent folders. + */ + // "forbidPhantomResolvableNodeModulesFolders": true, + + /** + * (UNDER DEVELOPMENT) For certain installation problems involving peer dependencies, PNPM cannot + * correctly satisfy versioning requirements without installing duplicate copies of a package inside the + * node_modules folder. This poses a problem for "workspace:*" dependencies, as they are normally + * installed by making a symlink to the local project source folder. PNPM's "injected dependencies" + * feature provides a model for copying the local project folder into node_modules, however copying + * must occur AFTER the dependency project is built and BEFORE the consuming project starts to build. + * The "pnpm-sync" tool manages this operation; see its documentation for details. + * Enable this experiment if you want "rush" and "rushx" commands to resync injected dependencies + * by invoking "pnpm-sync" during the build. + */ + // "usePnpmSyncForInjectedDependencies": true, + + /** + * If set to true, Rush will generate a `project-impact-graph.yaml` file in the repository root during `rush update`. + */ + // "generateProjectImpactGraphDuringRushUpdate": true, + + /** + * If true, when running in watch mode, Rush will check for phase scripts named `_phase::ipc` and run them instead + * of `_phase:` if they exist. The created child process will be provided with an IPC channel and expected to persist + * across invocations. + */ + // "useIPCScriptsInWatchMode": true, + + /** + * (UNDER DEVELOPMENT) The Rush alerts feature provides a way to send announcements to engineers + * working in the monorepo, by printing directly in the user's shell window when they invoke Rush commands. + * This ensures that important notices will be seen by anyone doing active development, since people often + * ignore normal discussion group messages or don't know to subscribe. + */ + // "rushAlerts": true, + + + /** + * When using cobuilds, this experiment allows uncacheable operations to benefit from cobuild orchestration without using the build cache. + */ + // "allowCobuildWithoutCache": true, + + /** + * By default, rush perform a full scan of the entire repository. For example, Rush runs `git status` to check for local file changes. + * When this toggle is enabled, Rush will only scan specific paths, significantly speeding up Git operations. + */ + // "enableSubpathScan": true, + + /** + * Rush has a policy that normally requires Rush projects to specify `workspace:*` in package.json when depending + * on other projects in the workspace, unless they are explicitly declared as `decoupledLocalDependencies` + * in rush.json. Enabling this experiment will remove that requirement for dependencies belonging to a different + * subspace. This is useful for large product groups who work in separate subspaces and generally prefer to consume + * each other's packages via the NPM registry. + */ + // "exemptDecoupledDependenciesBetweenSubspaces": false +} diff --git a/foundations/communication/common/config/rush/pnpm-config.json b/foundations/communication/common/config/rush/pnpm-config.json new file mode 100644 index 0000000000..f09b56ba1d --- /dev/null +++ b/foundations/communication/common/config/rush/pnpm-config.json @@ -0,0 +1,331 @@ +/** + * This configuration file provides settings specific to the PNPM package manager. + * More documentation is available on the Rush website: https://rushjs.io + * + * Rush normally looks for this file in `common/config/rush/pnpm-config.json`. However, + * if `subspacesEnabled` is true in subspaces.json, then Rush will instead first look + * for `common/config/subspaces//pnpm-config.json`. (If the file exists in both places, + * then the file under `common/config/rush` is ignored.) + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + + /** + * If true, then `rush install` and `rush update` will use the PNPM workspaces feature + * to perform the install, instead of the old model where Rush generated the symlinks + * for each projects's node_modules folder. + * + * When using workspaces, Rush will generate a `common/temp/pnpm-workspace.yaml` file referencing + * all local projects to install. Rush will also generate a `.pnpmfile.cjs` shim which implements + * Rush-specific features such as preferred versions. The user's `common/config/rush/.pnpmfile.cjs` + * is invoked by the shim. + * + * This option is strongly recommended. The default value is false. + */ + "useWorkspaces": true, + + /** + * This setting determines how PNPM chooses version numbers during `rush update`. + * For example, suppose `lib-x@3.0.0` depends on `"lib-y": "^1.2.3"` whose latest major + * releases are `1.8.9` and `2.3.4`. The resolution mode `lowest-direct` might choose + * `lib-y@1.2.3`, wheres `highest` will choose 1.8.9, and `time-based` will pick the + * highest compatible version at the time when `lib-x@3.0.0` itself was published (ensuring + * that the version could have been tested by the maintainer of "lib-x"). For local workspace + * projects, `time-based` instead works like `lowest-direct`, avoiding upgrades unless + * they are explicitly requested. Although `time-based` is the most robust option, it may be + * slightly slower with registries such as npmjs.com that have not implemented an optimization. + * + * IMPORTANT: Be aware that PNPM 8.0.0 initially defaulted to `lowest-direct` instead of + * `highest`, but PNPM reverted this decision in 8.6.12 because it caused confusion for users. + * Rush version 5.106.0 and newer avoids this confusion by consistently defaulting to + * `highest` when `resolutionMode` is not explicitly set in pnpm-config.json or .npmrc, + * regardless of your PNPM version. + * + * PNPM documentation: https://pnpm.io/npmrc#resolution-mode + * + * Possible values are: `highest`, `time-based`, and `lowest-direct`. + * The default is `highest`. + */ + // "resolutionMode": "time-based", + + /** + * This setting determines whether PNPM will automatically install (non-optional) + * missing peer dependencies instead of reporting an error. Doing so conveniently + * avoids the need to specify peer versions in package.json, but in a large monorepo + * this often creates worse problems. The reason is that peer dependency behavior + * is inherently complicated, and it is easier to troubleshoot consequences of an explicit + * version than an invisible heuristic. The original NPM RFC discussion pointed out + * some other problems with this feature: https://github.com/npm/rfcs/pull/43 + + * IMPORTANT: Without Rush, the setting defaults to true for PNPM 8 and newer; however, + * as of Rush version 5.109.0 the default is always false unless `autoInstallPeers` + * is specified in pnpm-config.json or .npmrc, regardless of your PNPM version. + + * PNPM documentation: https://pnpm.io/npmrc#auto-install-peers + + * The default value is false. + */ + // "autoInstallPeers": false, + + /** + * If true, then Rush will add the `--strict-peer-dependencies` command-line parameter when + * invoking PNPM. This causes `rush update` to fail if there are unsatisfied peer dependencies, + * which is an invalid state that can cause build failures or incompatible dependency versions. + * (For historical reasons, JavaScript package managers generally do not treat this invalid + * state as an error.) + * + * PNPM documentation: https://pnpm.io/npmrc#strict-peer-dependencies + * + * The default value is false to avoid legacy compatibility issues. + * It is strongly recommended to set `strictPeerDependencies=true`. + */ + // "strictPeerDependencies": true, + + /** + * Environment variables that will be provided to PNPM. + */ + // "environmentVariables": { + // "NODE_OPTIONS": { + // "value": "--max-old-space-size=4096", + // "override": false + // } + // }, + + /** + * Specifies the location of the PNPM store. There are two possible values: + * + * - `local` - use the `pnpm-store` folder in the current configured temp folder: + * `common/temp/pnpm-store` by default. + * - `global` - use PNPM's global store, which has the benefit of being shared + * across multiple repo folders, but the disadvantage of less isolation for builds + * (for example, bugs or incompatibilities when two repos use different releases of PNPM) + * + * In both cases, the store path can be overridden by the environment variable `RUSH_PNPM_STORE_PATH`. + * + * The default value is `local`. + */ + // "pnpmStore": "global", + + /** + * If true, then `rush install` will report an error if manual modifications + * were made to the PNPM shrinkwrap file without running `rush update` afterwards. + * + * This feature protects against accidental inconsistencies that may be introduced + * if the PNPM shrinkwrap file (`pnpm-lock.yaml`) is manually edited. When this + * feature is enabled, `rush update` will append a hash to the file as a YAML comment, + * and then `rush update` and `rush install` will validate the hash. Note that this + * does not prohibit manual modifications, but merely requires `rush update` be run + * afterwards, ensuring that PNPM can report or repair any potential inconsistencies. + * + * To temporarily disable this validation when invoking `rush install`, use the + * `--bypass-policy` command-line parameter. + * + * The default value is false. + */ + // "preventManualShrinkwrapChanges": true, + + /** + * When a project uses `workspace:` to depend on another Rush project, PNPM normally installs + * it by creating a symlink under `node_modules`. This generally works well, but in certain + * cases such as differing `peerDependencies` versions, symlinking may cause trouble + * such as incorrectly satisfied versions. For such cases, the dependency can be declared + * as "injected", causing PNPM to copy its built output into `node_modules` like a real + * install from a registry. Details here: https://rushjs.io/pages/advanced/injected_deps/ + * + * When using Rush subspaces, these sorts of versioning problems are much more likely if + * `workspace:` refers to a project from a different subspace. This is because the symlink + * would point to a separate `node_modules` tree installed by a different PNPM lockfile. + * A comprehensive solution is to enable `alwaysInjectDependenciesFromOtherSubspaces`, + * which automatically treats all projects from other subspaces as injected dependencies + * without having to manually configure them. + * + * NOTE: Use carefully -- excessive file copying can slow down the `rush install` and + * `pnpm-sync` operations if too many dependencies become injected. + * + * The default value is false. + */ + // "alwaysInjectDependenciesFromOtherSubspaces": false, + + /** + * Defines the policies to be checked for the `pnpm-lock.yaml` file. + */ + "pnpmLockfilePolicies": { + + /** + * This policy will cause "rush update" to report an error if `pnpm-lock.yaml` contains + * any SHA1 integrity hashes. + * + * For each NPM dependency, `pnpm-lock.yaml` normally stores an `integrity` hash. Although + * its main purpose is to detect corrupted or truncated network requests, this hash can also + * serve as a security fingerprint to protect against attacks that would substitute a + * malicious tarball, for example if a misconfigured .npmrc caused a machine to accidentally + * download a matching package name+version from npmjs.com instead of the private NPM registry. + * NPM originally used a SHA1 hash; this was insecure because an attacker can too easily craft + * a tarball with a matching fingerprint. For this reason, NPM later deprecated SHA1 and + * instead adopted a cryptographically strong SHA512 hash. Nonetheless, SHA1 hashes can + * occasionally reappear during "rush update", for example due to missing metadata fallbacks + * (https://github.com/orgs/pnpm/discussions/6194) or an incompletely migrated private registry. + * The `disallowInsecureSha1` policy prevents this, avoiding potential security/compliance alerts. + */ + // "disallowInsecureSha1": { + // /** + // * Enables the "disallowInsecureSha1" policy. The default value is false. + // */ + // "enabled": true, + // + // /** + // * In rare cases, a private NPM registry may continue to serve SHA1 hashes for very old + // * package versions, perhaps due to a caching issue or database migration glitch. To avoid + // * having to disable the "disallowInsecureSha1" policy for the entire monorepo, the problematic + // * package versions can be individually ignored. The "exemptPackageVersions" key is the + // * package name, and the array value lists exact version numbers to be ignored. + // */ + // "exemptPackageVersions": { + // "example1": ["1.0.0"], + // "example2": ["2.0.0", "2.0.1"] + // } + // } + }, + + /** + * The "globalOverrides" setting provides a simple mechanism for overriding version selections + * for all dependencies of all projects in the monorepo workspace. The settings are copied + * into the `pnpm.overrides` field of the `common/temp/package.json` file that is generated + * by Rush during installation. + * + * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by + * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, + * and `globalOverrides` has lowest precedence. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmoverrides + */ + "globalOverrides": { + // "example1": "^1.0.0", + // "example2": "npm:@company/example2@^1.0.0" + }, + + /** + * The `globalPeerDependencyRules` setting provides various settings for suppressing validation errors + * that are reported during installation with `strictPeerDependencies=true`. The settings are copied + * into the `pnpm.peerDependencyRules` field of the `common/temp/package.json` file that is generated + * by Rush during installation. + * + * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by + * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, + * and `globalOverrides` has lowest precedence. + * + * https://pnpm.io/package_json#pnpmpeerdependencyrules + */ + "globalPeerDependencyRules": { + // "ignoreMissing": ["@eslint/*"], + // "allowedVersions": { "react": "17" }, + // "allowAny": ["@babel/*"] + }, + + /** + * The `globalPackageExtension` setting provides a way to patch arbitrary package.json fields + * for any PNPM dependency of the monorepo. The settings are copied into the `pnpm.packageExtensions` + * field of the `common/temp/package.json` file that is generated by Rush during installation. + * The `globalPackageExtension` setting has similar capabilities as `.pnpmfile.cjs` but without + * the downsides of an executable script (nondeterminism, unreliable caching, performance concerns). + * + * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by + * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, + * and `globalOverrides` has lowest precedence. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmpackageextensions + */ + "globalPackageExtensions": { + // "fork-ts-checker-webpack-plugin": { + // "dependencies": { + // "@babel/core": "1" + // }, + // "peerDependencies": { + // "eslint": ">= 6" + // }, + // "peerDependenciesMeta": { + // "eslint": { + // "optional": true + // } + // } + // } + }, + + /** + * The `globalNeverBuiltDependencies` setting suppresses the `preinstall`, `install`, and `postinstall` + * lifecycle events for the specified NPM dependencies. This is useful for scripts with poor practices + * such as downloading large binaries without retries or attempting to invoke OS tools such as + * a C++ compiler. (PNPM's terminology refers to these lifecycle events as "building" a package; + * it has nothing to do with build system operations such as `rush build` or `rushx build`.) + * The settings are copied into the `pnpm.neverBuiltDependencies` field of the `common/temp/package.json` + * file that is generated by Rush during installation. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmneverbuiltdependencies + */ + "globalNeverBuiltDependencies": [ + // "fsevents" + ], + + /** + * The `globalIgnoredOptionalDependencies` setting suppresses the installation of optional NPM + * dependencies specified in the list. This is useful when certain optional dependencies are + * not needed in your environment, such as platform-specific packages or dependencies that + * fail during installation but are not critical to your project. + * These settings are copied into the `pnpm.overrides` field of the `common/temp/package.json` + * file that is generated by Rush during installation, instructing PNPM to ignore the specified + * optional dependencies. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmignoredoptionaldependencies + */ + "globalIgnoredOptionalDependencies": [ + // "fsevents" + ], + + /** + * The `globalAllowedDeprecatedVersions` setting suppresses installation warnings for package + * versions that the NPM registry reports as being deprecated. This is useful if the + * deprecated package is an indirect dependency of an external package that has not released a fix. + * The settings are copied into the `pnpm.allowedDeprecatedVersions` field of the `common/temp/package.json` + * file that is generated by Rush during installation. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmalloweddeprecatedversions + * + * If you are working to eliminate a deprecated version, it's better to specify `allowedDeprecatedVersions` + * in the package.json file for individual Rush projects. + */ + "globalAllowedDeprecatedVersions": { + // "request": "*" + }, + + + /** + * (THIS FIELD IS MACHINE GENERATED) The "globalPatchedDependencies" field is updated automatically + * by the `rush-pnpm patch-commit` command. It is a dictionary, where the key is an NPM package name + * and exact version, and the value is a relative path to the associated patch file. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmpatcheddependencies + */ + "globalPatchedDependencies": { }, + + /** + * (USE AT YOUR OWN RISK) This is a free-form property bag that will be copied into + * the `common/temp/package.json` file that is generated by Rush during installation. + * This provides a way to experiment with new PNPM features. These settings will override + * any other Rush configuration associated with a given JSON field except for `.pnpmfile.cjs`. + * + * USAGE OF THIS SETTING IS NOT SUPPORTED BY THE RUSH MAINTAINERS AND MAY CAUSE RUSH + * TO MALFUNCTION. If you encounter a missing PNPM setting that you believe should + * be supported, please create a GitHub issue or PR. Note that Rush does not aim to + * support every possible PNPM setting, but rather to promote a battle-tested installation + * strategy that is known to provide a good experience for large teams with lots of projects. + */ + "unsupportedPackageJsonSettings": { + // "dependencies": { + // "not-a-good-practice": "*" + // }, + // "scripts": { + // "do-something": "echo Also not a good practice" + // }, + // "pnpm": { "futurePnpmFeature": true } + } +} diff --git a/foundations/communication/common/config/rush/pnpm-lock.yaml b/foundations/communication/common/config/rush/pnpm-lock.yaml new file mode 100644 index 0000000000..8182ddd7de --- /dev/null +++ b/foundations/communication/common/config/rush/pnpm-lock.yaml @@ -0,0 +1,6444 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +pnpmfileChecksum: avk5jcsa6uxo2n5cahoirjiuw4 + +importers: + + .: {} + + ../../packages/client-query: + dependencies: + '@hcengineering/communication-query': + specifier: workspace:^0.7.11 + version: link:../query + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + '@hcengineering/hulylake-client': + specifier: ^0.7.17 + version: 0.7.17 + fast-equals: + specifier: ^5.2.2 + version: 5.3.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.9 + version: 0.25.10 + esbuild-plugin-copy: + specifier: ~2.1.1 + version: 2.1.1(esbuild@0.25.10) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/cockroach: + dependencies: + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-shared': + specifier: workspace:^0.7.11 + version: link:../shared + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + postgres: + specifier: ^3.4.7 + version: 3.4.7 + uuid: + specifier: ^8.3.2 + version: 8.3.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@types/uuid': + specifier: ^8.3.1 + version: 8.3.4 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.9 + version: 0.25.10 + esbuild-plugin-copy: + specifier: ~2.1.1 + version: 2.1.1(esbuild@0.25.10) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/query: + dependencies: + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-shared': + specifier: workspace:^0.7.11 + version: link:../shared + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + '@hcengineering/hulylake-client': + specifier: ^0.7.17 + version: 0.7.17 + fast-equals: + specifier: ^5.2.2 + version: 5.3.2 + uuid: + specifier: ^8.3.2 + version: 8.3.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/uuid': + specifier: ^8.3.1 + version: 8.3.4 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.9 + version: 0.25.10 + esbuild-plugin-copy: + specifier: ~2.1.1 + version: 2.1.1(esbuild@0.25.10) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/rest-client: + dependencies: + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-shared': + specifier: workspace:^0.7.11 + version: link:../shared + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + '@hcengineering/core': + specifier: ^0.7.19 + version: 0.7.19 + snappyjs: + specifier: ^0.7.0 + version: 0.7.0 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/snappyjs': + specifier: ^0.7.1 + version: 0.7.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.9 + version: 0.25.10 + esbuild-plugin-copy: + specifier: ~2.1.1 + version: 2.1.1(esbuild@0.25.10) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/sdk-types: + dependencies: + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + '@hcengineering/core': + specifier: ^0.7.19 + version: 0.7.19 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.9 + version: 0.25.10 + esbuild-plugin-copy: + specifier: ~2.1.1 + version: 2.1.1(esbuild@0.25.10) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/server: + dependencies: + '@hcengineering/account-client': + specifier: ^0.7.18 + version: 0.7.18 + '@hcengineering/communication-cockroach': + specifier: workspace:^0.7.11 + version: link:../cockroach + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-shared': + specifier: workspace:^0.7.11 + version: link:../shared + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + '@hcengineering/core': + specifier: ^0.7.19 + version: 0.7.19 + '@hcengineering/hulylake-client': + specifier: ^0.7.17 + version: 0.7.17 + '@hcengineering/server-token': + specifier: ^0.7.17 + version: 0.7.17 + '@hcengineering/text-core': + specifier: ^0.7.18 + version: 0.7.18 + '@hcengineering/text-markdown': + specifier: ^0.7.18 + version: 0.7.18 + uuid: + specifier: ^8.3.2 + version: 8.3.2 + zod: + specifier: ^3.22.4 + version: 3.25.76 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@types/uuid': + specifier: ^8.3.1 + version: 8.3.4 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.9 + version: 0.25.10 + esbuild-plugin-copy: + specifier: ~2.1.1 + version: 2.1.1(esbuild@0.25.10) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/shared: + dependencies: + '@hcengineering/communication-sdk-types': + specifier: workspace:^0.7.12 + version: link:../sdk-types + '@hcengineering/communication-types': + specifier: workspace:^0.7.12 + version: link:../types + '@hcengineering/hulylake-client': + specifier: ^0.7.17 + version: 0.7.17 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.9 + version: 0.25.10 + esbuild-plugin-copy: + specifier: ~2.1.1 + version: 2.1.1(esbuild@0.25.10) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/types: + dependencies: + '@hcengineering/core': + specifier: ^0.7.19 + version: 0.7.19 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.25.9 + version: 0.25.10 + esbuild-plugin-copy: + specifier: ~2.1.1 + version: 2.1.1(esbuild@0.25.10) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + ../scripts: + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.24.2 + version: 0.24.2 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@formatjs/ecma402-abstract@2.3.5': + resolution: {integrity: sha512-1HTESOq1IUa23g1lFZEGIXsfZKZOwWmB9RROwGn+xariiQnd++wwTMvlRAbZ8wtXRHFUamJPxsKcxpSzeCvFWQ==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.3': + resolution: {integrity: sha512-H/KfWSosaiDiOaW4nHe1Fn4Cgzm+oFQ8giTmB5RJzTBNSMmd+j2NVrvvZHAmlxJHcuOelzKBLjQ2EDcyH4NSWw==} + + '@formatjs/icu-skeleton-parser@1.8.15': + resolution: {integrity: sha512-qNrKxWJmnWxin5U4A4Evy7C0rgRiNw3IqXu9OGuT31B8lDxBGl+OgT8kcq0ZVKK0gqA4l4SQB9x+SFAvLT5hcQ==} + + '@formatjs/intl-localematcher@0.6.2': + resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + + '@hcengineering/account-client@0.7.18': + resolution: {integrity: sha512-o7iU2cx2rRtkv3ntvbMaw4f8Wk6of7Cot+y6MAsHXTlELLStcKjCQcLRj7abne0sdN/N3luaNVkJBqfu4JpHHw==} + + '@hcengineering/analytics@0.7.17': + resolution: {integrity: sha512-AzB2DvmDctEJge8VeAHS3YyRYij/QQlARxa2OCNiY+Vv+2+OsQk0NZsw1yqZqsMkVJ40FTUrzOkI7UdM6RPO1Q==} + + '@hcengineering/core@0.7.19': + resolution: {integrity: sha512-BMkVkVXMU8qmbR2KAeMZ3YIMfa07/AzdCEY7PNn7dfAZyc63vrLz+Jl0CPvN7cq9p6x/WBkMfS+eKFwtvhZIUA==} + + '@hcengineering/hulylake-client@0.7.17': + resolution: {integrity: sha512-TPqdc5nd7xV73Y1B2EJ7GxpWazp9ACV1AqHlm6ebYwU1CzXQvbuJxF68itbHmPiNiCmmuiDruJmjnyLGwL18Kw==} + + '@hcengineering/measurements@0.7.18': + resolution: {integrity: sha512-4MxYthJhNWPfR389lN5cIFtrhzgmhi39Fx/dqIOOYF+6g0WWA6GtWXi+ORRM8l/AWKSastZqsWxMdBbT+0htdg==} + + '@hcengineering/platform-rig@0.7.19': + resolution: {integrity: sha512-3Fi5nU+nEPjzzcm3FIfEZU+ZgdkiSfL46ojbj42jdXnWMkxew2aZZVtfmIpPnMUP9AMZlj2/EA+yOcMzfY/VmA==} + hasBin: true + + '@hcengineering/platform@0.7.17': + resolution: {integrity: sha512-PJH38bQ3yZgP52vHV7r8Y+JkCuH0nV9kic/CNt4XuNXQp9nTjzBlDnl+1IrCo6hU+x3QweKVIEzMXzU9J4hhEA==} + + '@hcengineering/retry@0.7.17': + resolution: {integrity: sha512-JPIGoWP3RHTET0laQV4rqlNxjn3tT9O54XNxCxsxhTFF5xeC02XxmW/xV4Gh6QWE5/QCXY5DD3Sz3ild3Cr7Uw==} + + '@hcengineering/server-token@0.7.17': + resolution: {integrity: sha512-MDoGzVOTScJXnx2jHL63szclhiqPOKEXk7bmUj6i/HLUQoU6UAl9TTq1Zx/TomyzFQUx8Vkuj0j0bJMUcmLXmQ==} + + '@hcengineering/text-core@0.7.18': + resolution: {integrity: sha512-UZ/ZMwVcf60WpnYYcsA7RqYE4JlHnAGVdmUDHX2SYzXXvDIK7deGWD9AKyK4yqI1mb19ccyCr5fGyGoLxx9k/g==} + + '@hcengineering/text-html@0.7.18': + resolution: {integrity: sha512-hGPgptTk2Kfvlg8lAniaFe8jpf3RD4mZD0eL6CoPlXTKylUOklOGHFGWId+QjUYGPLBVkODPErmZxrZKbkRSWg==} + + '@hcengineering/text-markdown@0.7.18': + resolution: {integrity: sha512-GEslsa/gPp5NH+VPg/G8vOVzt9XdEUr8myoFDBz0LGlQVs6veAEv1psUdOynKeHV/ntF3qmN3hSHg0LrMyoGfA==} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@22.18.10': + resolution: {integrity: sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==} + + '@types/pug@2.0.10': + resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} + + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + + '@types/snappyjs@0.7.1': + resolution: {integrity: sha512-OxjzJ6cQZstysMh6PEwZWmK9qlKZyezHJKOkcUkZDooSFuog2votUEKkxMaTq51UQF3cJkXKQ+XGlj4FSl8JQQ==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/uuid@8.3.4': + resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@typescript-eslint/eslint-plugin@6.21.0': + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@6.21.0': + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@6.21.0': + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/type-utils@6.21.0': + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@6.21.0': + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/typescript-estree@6.21.0': + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@6.21.0': + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@6.21.0': + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.8.12: + resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.26.3: + resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + builtins@5.1.0: + resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001748: + resolution: {integrity: sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + code-red@1.0.4: + resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + + dedent@1.7.0: + resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.232: + resolution: {integrity: sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es6-promise@3.3.1: + resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} + + esbuild-plugin-copy@2.1.1: + resolution: {integrity: sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw==} + peerDependencies: + esbuild: '>= 0.14.0' + + esbuild-svelte@0.9.3: + resolution: {integrity: sha512-CgEcGY1r/d16+aggec3czoFBEBaYIrFOnMxpsO6fWNaNEqHregPN5DLAPZDqrL7rXDNplW+WMu8s3GMq9FqgJA==} + engines: {node: '>=18'} + peerDependencies: + esbuild: '>=0.17.0' + svelte: '>=4.2.1 <6' + + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-standard-with-typescript@40.0.0: + resolution: {integrity: sha512-GXUJcwIXiTQaS3H4etv8a1lejVVdZYaxZNz3g7vt6GoJosQqMTurbmSC4FVGyHiGT/d1TjFr3+47A3xsHhsG+Q==} + deprecated: Please use eslint-config-love, instead. + peerDependencies: + '@typescript-eslint/eslint-plugin': ^6.4.0 + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: '^15.0.0 || ^16.0.0 ' + eslint-plugin-promise: ^6.0.0 + typescript: '*' + + eslint-config-standard@17.1.0: + resolution: {integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: '^15.0.0 || ^16.0.0 ' + eslint-plugin-promise: ^6.0.0 + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + eslint: '*' + peerDependenciesMeta: + eslint: + optional: true + + eslint-plugin-es@4.1.0: + resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + + eslint-plugin-n@15.7.0: + resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==} + engines: {node: '>=12.22.0'} + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-promise@6.6.0: + resolution: {integrity: sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-svelte@2.46.1: + resolution: {integrity: sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + + eslint-utils@3.0.0: + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + + eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + + eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@5.3.2: + resolution: {integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hash-it@6.0.0: + resolution: {integrity: sha512-KHzmSFx1KwyMPw0kXeeUD752q/Kfbzhy6dAZrjXV9kAIXGqzGvv8vhkUqj+2MGZldTo0IBpw6v7iWE7uxsvH0w==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + intl-messageformat@10.7.17: + resolution: {integrity: sha512-0Ugaf65B2J76rb31drgNF1l6bGEDkbIiYc2Glx6jaZINHnwa5kDRGy8KXYuA+/8P4G0c9prAFhfVhQJJfzUuvQ==} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jwt-simple@0.5.6: + resolution: {integrity: sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==} + engines: {node: '>= 0.4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + known-css-properties@0.35.0: + resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.23: + resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-svelte@3.4.0: + resolution: {integrity: sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + sander@0.5.1: + resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + snappyjs@0.7.0: + resolution: {integrity: sha512-u5iEEXkMe2EInQio6Wv9LWHOQYRDbD2O9hzS27GpT/lwfIQhTCnHCTqedqHIHe9ZcvQo+9au6vngQayipz1NYw==} + + sorcery@0.11.1: + resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte-eslint-parser@0.33.1: + resolution: {integrity: sha512-vo7xPGTlKBGdLH8T5L64FipvTrqv3OQRx9d2z5X05KKZDlF4rQk8KViZO4flKERY+5BiVdOh7zZ7JGJWo5P0uA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte-eslint-parser@0.43.0: + resolution: {integrity: sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte-preprocess@5.1.4: + resolution: {integrity: sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + '@babel/core': ^7.10.2 + coffeescript: ^2.5.1 + less: ^3.11.3 || ^4.0.0 + postcss: ^7 || ^8 + postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + pug: ^3.0.0 + sass: ^1.26.8 + stylus: ^0.55.0 + sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 + svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 + typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' + peerDependenciesMeta: + '@babel/core': + optional: true + coffeescript: + optional: true + less: + optional: true + postcss: + optional: true + postcss-load-config: + optional: true + pug: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + typescript: + optional: true + + svelte2tsx@0.7.45: + resolution: {integrity: sha512-cSci+mYGygYBHIZLHlm/jYlEc1acjAHqaQaDFHdEBpUueM9kSTnPpvPtSl5VkJOU1qSJ7h1K+6F/LIUYiqC8VA==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 + + svelte@4.2.20: + resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==} + engines: {node: '>=16'} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-jest@29.4.5: + resolution: {integrity: sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@0.2.3': {} + + '@esbuild/aix-ppc64@0.24.2': + optional: true + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.24.2': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.24.2': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.24.2': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.24.2': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.24.2': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.24.2': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.24.2': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.24.2': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.24.2': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.24.2': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.24.2': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.24.2': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.24.2': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.24.2': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.24.2': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.24.2': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.24.2': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.24.2': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.24.2': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.24.2': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.24.2': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.24.2': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.24.2': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.24.2': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@formatjs/ecma402-abstract@2.3.5': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.3': + dependencies: + '@formatjs/ecma402-abstract': 2.3.5 + '@formatjs/icu-skeleton-parser': 1.8.15 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.15': + dependencies: + '@formatjs/ecma402-abstract': 2.3.5 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.2': + dependencies: + tslib: 2.8.1 + + '@hcengineering/account-client@0.7.18': + dependencies: + '@hcengineering/core': 0.7.19 + '@hcengineering/platform': 0.7.17 + + '@hcengineering/analytics@0.7.17': + dependencies: + '@hcengineering/platform': 0.7.17 + + '@hcengineering/core@0.7.19': + dependencies: + '@hcengineering/analytics': 0.7.17 + '@hcengineering/measurements': 0.7.18 + '@hcengineering/platform': 0.7.17 + fast-equals: 5.3.2 + + '@hcengineering/hulylake-client@0.7.17': + dependencies: + '@hcengineering/core': 0.7.19 + '@hcengineering/retry': 0.7.17 + + '@hcengineering/measurements@0.7.18': {} + + '@hcengineering/platform-rig@0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6)': + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: 0.25.10 + esbuild-plugin-copy: 2.1.1(esbuild@0.25.10) + esbuild-svelte: 0.9.3(esbuild@0.25.10)(svelte@4.2.20) + eslint: 8.57.1 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: 2.32.0(eslint@8.57.1) + eslint-plugin-n: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: 2.46.1(eslint@8.57.1)(svelte@4.2.20) + prettier: 3.6.2 + prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@4.2.20) + svelte: 4.2.20 + svelte-eslint-parser: 0.33.1(svelte@4.2.20) + svelte-preprocess: 5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6)(svelte@4.2.20)(typescript@5.9.3) + svelte2tsx: 0.7.45(svelte@4.2.20)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - '@babel/core' + - coffeescript + - less + - postcss + - postcss-load-config + - pug + - sass + - stylus + - sugarss + - supports-color + - ts-node + + '@hcengineering/platform@0.7.17': + dependencies: + intl-messageformat: 10.7.17 + + '@hcengineering/retry@0.7.17': {} + + '@hcengineering/server-token@0.7.17': + dependencies: + '@hcengineering/core': 0.7.19 + '@hcengineering/platform': 0.7.17 + jwt-simple: 0.5.6 + uuid: 8.3.2 + + '@hcengineering/text-core@0.7.18': + dependencies: + '@hcengineering/core': 0.7.19 + fast-equals: 5.3.2 + hash-it: 6.0.0 + + '@hcengineering/text-html@0.7.18': + dependencies: + '@hcengineering/text-core': 0.7.18 + htmlparser2: 9.1.0 + + '@hcengineering/text-markdown@0.7.18': + dependencies: + '@hcengineering/text-core': 0.7.18 + '@hcengineering/text-html': 0.7.18 + fast-equals: 5.3.2 + markdown-it: 14.1.0 + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.18.10) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.18.10 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 22.18.10 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.28.4 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.18.10 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@rtsao/scc@1.1.0': {} + + '@sinclair/typebox@0.27.8': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/estree@1.0.8': {} + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.18.10 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@22.18.10': + dependencies: + undici-types: 6.21.0 + + '@types/pug@2.0.10': {} + + '@types/semver@7.7.1': {} + + '@types/snappyjs@0.7.1': {} + + '@types/stack-utils@2.0.3': {} + + '@types/uuid@8.3.4': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + semver: 7.7.2 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + eslint: 8.57.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@6.21.0': {} + + '@typescript-eslint/typescript-estree@6.21.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.7.2 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.1 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + eslint: 8.57.1 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.3.0': {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array-union@2.1.0: {} + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axobject-query@4.1.0: {} + + babel-jest@29.7.0(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.28.4) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4) + + babel-preset-jest@29.6.3(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.8.12: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.26.3: + dependencies: + baseline-browser-mapping: 2.8.12 + caniuse-lite: 1.0.30001748 + electron-to-chromium: 1.5.232 + node-releases: 2.0.23 + update-browserslist-db: 1.1.3(browserslist@4.26.3) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-crc32@1.0.0: {} + + buffer-from@1.1.2: {} + + builtins@5.1.0: + dependencies: + semver: 7.7.2 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001748: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.3: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + co@4.6.0: {} + + code-red@1.0.4: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@types/estree': 1.0.8 + acorn: 8.15.0 + estree-walker: 3.0.3 + periscopic: 3.1.0 + + collect-v8-coverage@1.0.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + create-jest@29.7.0(@types/node@22.18.10): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.18.10) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + dedent-js@1.0.1: {} + + dedent@1.7.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + detect-indent@6.1.0: {} + + detect-newline@3.1.0: {} + + diff-sequences@29.6.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.232: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + entities@4.5.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es6-promise@3.3.1: {} + + esbuild-plugin-copy@2.1.1(esbuild@0.25.10): + dependencies: + chalk: 4.1.2 + chokidar: 3.6.0 + esbuild: 0.25.10 + fs-extra: 10.1.0 + globby: 11.1.0 + + esbuild-svelte@0.9.3(esbuild@0.25.10)(svelte@4.2.20): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + esbuild: 0.25.10 + svelte: 4.2.20 + + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + escalade@3.2.0: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-compat-utils@0.5.1(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + semver: 7.7.2 + + eslint-config-standard-with-typescript@40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(eslint@8.57.1) + eslint-plugin-n: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: 6.6.0(eslint@8.57.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-plugin-import: 2.32.0(eslint@8.57.1) + eslint-plugin-n: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: 6.6.0(eslint@8.57.1) + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + + eslint-module-utils@2.12.1(eslint@8.57.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + eslint: 8.57.1 + + eslint-plugin-es@4.1.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + + eslint-plugin-import@2.32.0(eslint@8.57.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + + eslint-plugin-n@15.7.0(eslint@8.57.1): + dependencies: + builtins: 5.1.0 + eslint: 8.57.1 + eslint-plugin-es: 4.1.0(eslint@8.57.1) + eslint-utils: 3.0.0(eslint@8.57.1) + ignore: 5.3.2 + is-core-module: 2.16.1 + minimatch: 3.1.2 + resolve: 1.22.10 + semver: 7.7.2 + + eslint-plugin-promise@6.6.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-svelte@2.46.1(eslint@8.57.1)(svelte@4.2.20): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@jridgewell/sourcemap-codec': 1.5.5 + eslint: 8.57.1 + eslint-compat-utils: 0.5.1(eslint@8.57.1) + esutils: 2.0.3 + known-css-properties: 0.35.0 + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6) + postcss-safe-parser: 6.0.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + semver: 7.7.2 + svelte-eslint-parser: 0.43.0(svelte@4.2.20) + optionalDependencies: + svelte: 4.2.20 + transitivePeerDependencies: + - ts-node + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-utils@2.1.0: + dependencies: + eslint-visitor-keys: 1.3.0 + + eslint-utils@3.0.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 2.1.0 + + eslint-visitor-keys@1.3.0: {} + + eslint-visitor-keys@2.1.0: {} + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + fast-deep-equal@3.1.3: {} + + fast-equals@5.3.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hash-it@6.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-escaper@2.0.2: {} + + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + human-signals@2.1.0: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + intl-messageformat@10.7.17: + dependencies: + '@formatjs/ecma402-abstract': 2.3.5 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.3 + tslib: 2.8.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.0 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@22.18.10): + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.18.10) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.18.10) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@22.18.10): + dependencies: + '@babel/core': 7.28.4 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.4) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.18.10 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.18.10 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.10 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.28.4 + '@babel/generator': 7.28.3 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) + '@babel/types': 7.28.4 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.18.10 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.18.10): + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.18.10) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jwt-simple@0.5.6: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + known-css-properties@0.35.0: {} + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@2.1.0: {} + + lines-and-columns@1.2.4: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + locate-character@3.0.0: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@4.0.0: + dependencies: + semver: 7.7.2 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + math-intrinsics@1.1.0: {} + + mdn-data@2.0.30: {} + + mdurl@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@2.1.0: {} + + min-indent@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + node-int64@0.4.0: {} + + node-releases@2.0.23: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + periscopic@3.1.0: + dependencies: + '@types/estree': 1.0.8 + estree-walker: 3.0.3 + is-reference: 3.0.3 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + possible-typed-array-names@1.1.0: {} + + postcss-load-config@3.1.4(postcss@8.5.6): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.2 + optionalDependencies: + postcss: 8.5.6 + + postcss-safe-parser@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres@3.4.7: {} + + prelude-ls@1.2.1: {} + + prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@4.2.20): + dependencies: + prettier: 3.6.2 + svelte: 4.2.20 + + prettier@3.6.2: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + queue-microtask@1.2.3: {} + + react-is@18.3.1: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpp@3.2.0: {} + + require-directory@2.1.1: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + sander@0.5.1: + dependencies: + es6-promise: 3.3.1 + graceful-fs: 4.2.11 + mkdirp: 0.5.6 + rimraf: 2.7.1 + + scule@1.3.0: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + semver@7.7.3: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + snappyjs@0.7.0: {} + + sorcery@0.11.1: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + buffer-crc32: 1.0.0 + minimist: 1.2.8 + sander: 0.5.1 + + source-map-js@1.2.1: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + sprintf-js@1.0.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svelte-eslint-parser@0.33.1(svelte@4.2.20): + dependencies: + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + postcss: 8.5.6 + postcss-scss: 4.0.9(postcss@8.5.6) + optionalDependencies: + svelte: 4.2.20 + + svelte-eslint-parser@0.43.0(svelte@4.2.20): + dependencies: + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + postcss: 8.5.6 + postcss-scss: 4.0.9(postcss@8.5.6) + optionalDependencies: + svelte: 4.2.20 + + svelte-preprocess@5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6)(svelte@4.2.20)(typescript@5.9.3): + dependencies: + '@types/pug': 2.0.10 + detect-indent: 6.1.0 + magic-string: 0.30.19 + sorcery: 0.11.1 + strip-indent: 3.0.0 + svelte: 4.2.20 + optionalDependencies: + '@babel/core': 7.28.4 + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6) + typescript: 5.9.3 + + svelte2tsx@0.7.45(svelte@4.2.20)(typescript@5.9.3): + dependencies: + dedent-js: 1.0.1 + scule: 1.3.0 + svelte: 4.2.20 + typescript: 5.9.3 + + svelte@4.2.20: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + code-red: 1.0.4 + css-tree: 2.3.1 + estree-walker: 3.0.3 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.19 + periscopic: 3.1.0 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-table@0.2.0: {} + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@1.4.3(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-jest@29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@22.18.10) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.4 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.4) + esbuild: 0.25.10 + jest-util: 29.7.0 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + uc.micro@2.1.0: {} + + uglify-js@3.19.3: + optional: true + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + universalify@2.0.1: {} + + update-browserslist-db@1.1.3(browserslist@4.26.3): + dependencies: + browserslist: 4.26.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + uuid@8.3.2: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@1.10.2: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zod@3.25.76: {} diff --git a/foundations/communication/common/config/rush/repo-state.json b/foundations/communication/common/config/rush/repo-state.json new file mode 100644 index 0000000000..0e7b144099 --- /dev/null +++ b/foundations/communication/common/config/rush/repo-state.json @@ -0,0 +1,4 @@ +// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. +{ + "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" +} diff --git a/foundations/communication/common/config/rush/rush-plugins.json b/foundations/communication/common/config/rush/rush-plugins.json new file mode 100644 index 0000000000..752e373213 --- /dev/null +++ b/foundations/communication/common/config/rush/rush-plugins.json @@ -0,0 +1,29 @@ +/** + * This configuration file manages Rush's plugin feature. + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugins.schema.json", + "plugins": [ + /** + * Each item configures a plugin to be loaded by Rush. + */ + // { + // /** + // * The name of the NPM package that provides the plugin. + // */ + // "packageName": "@scope/my-rush-plugin", + // /** + // * The name of the plugin. This can be found in the "pluginName" + // * field of the "rush-plugin-manifest.json" file in the NPM package folder. + // */ + // "pluginName": "my-plugin-name", + // /** + // * The name of a Rush autoinstaller that will be used for installation, which + // * can be created using "rush init-autoinstaller". Add the plugin's NPM package + // * to the package.json "dependencies" of your autoinstaller, then run + // * "rush update-autoinstaller". + // */ + // "autoinstallerName": "rush-plugins" + // } + ] +} \ No newline at end of file diff --git a/foundations/communication/common/config/rush/subspaces.json b/foundations/communication/common/config/rush/subspaces.json new file mode 100644 index 0000000000..d3c3ae8c51 --- /dev/null +++ b/foundations/communication/common/config/rush/subspaces.json @@ -0,0 +1,35 @@ +/** + * This configuration file manages the experimental "subspaces" feature for Rush, + * which allows multiple PNPM lockfiles to be used in a single Rush workspace. + * For full documentation, please see https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json", + + /** + * Set this flag to "true" to enable usage of subspaces. + */ + "subspacesEnabled": false, + + /** + * (DEPRECATED) This is a temporary workaround for migrating from an earlier prototype + * of this feature: https://github.com/microsoft/rushstack/pull/3481 + * It allows subspaces with only one project to store their config files in the project folder. + */ + "splitWorkspaceCompatibility": false, + + /** + * When a command such as "rush update" is invoked without the "--subspace" or "--to" + * parameters, Rush will install all subspaces. In a huge monorepo with numerous subspaces, + * this would be extremely slow. Set "preventSelectingAllSubspaces" to true to avoid this + * mistake by always requiring selection parameters for commands such as "rush update". + */ + "preventSelectingAllSubspaces": false, + + /** + * The list of subspace names, which should be lowercase alphanumeric words separated by + * hyphens, for example "my-subspace". The corresponding config files will have paths + * such as "common/config/subspaces/my-subspace/package-lock.yaml". + */ + "subspaceNames": [] +} diff --git a/foundations/communication/common/config/rush/version-policies.json b/foundations/communication/common/config/rush/version-policies.json new file mode 100644 index 0000000000..d03b73a3b6 --- /dev/null +++ b/foundations/communication/common/config/rush/version-policies.json @@ -0,0 +1,102 @@ +/** + * This is configuration file is used for advanced publishing configurations with Rush. + * More documentation is available on the Rush website: https://rushjs.io + */ + +/** + * A list of version policy definitions. A "version policy" is a custom package versioning + * strategy that affects "rush change", "rush version", and "rush publish". The strategy applies + * to a set of projects that are specified using the "versionPolicyName" field in rush.json. + */ +[ + // { + // /** + // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). + // * + // * The "lockStepVersion" mode specifies that the projects will use "lock-step versioning". This + // * strategy is appropriate for a set of packages that act as selectable components of a + // * unified product. The entire set of packages are always published together, and always share + // * the same NPM version number. When the packages depend on other packages in the set, the + // * SemVer range is usually restricted to a single version. + // */ + // "definitionName": "lockStepVersion", + // + // /** + // * (Required) The name that will be used for the "versionPolicyName" field in rush.json. + // * This name is also used command-line parameters such as "--version-policy" + // * and "--to-version-policy". + // */ + // "policyName": "MyBigFramework", + // + // /** + // * (Required) The current version. All packages belonging to the set should have this version + // * in the current branch. When bumping versions, Rush uses this to determine the next version. + // * (The "version" field in package.json is NOT considered.) + // */ + // "version": "1.0.0", + // + // /** + // * (Required) The type of bump that will be performed when publishing the next release. + // * When creating a release branch in Git, this field should be updated according to the + // * type of release. + // * + // * Valid values are: "prerelease", "preminor", "minor", "patch", "major" + // */ + // "nextBump": "prerelease", + // + // /** + // * (Optional) If specified, all packages in the set share a common CHANGELOG.md file. + // * This file is stored with the specified "main" project, which must be a member of the set. + // * + // * If this field is omitted, then a separate CHANGELOG.md file will be maintained for each + // * package in the set. + // */ + // "mainProject": "my-app", + // + // /** + // * (Optional) If enabled, the "rush change" command will prompt the user for their email address + // * and include it in the JSON change files. If an organization maintains multiple repos, tracking + // * this contact information may be useful for a service that automatically upgrades packages and + // * needs to notify engineers whose change may be responsible for a downstream build break. It might + // * also be useful for crediting contributors. Rush itself does not do anything with the collected + // * email addresses. The default value is "false". + // */ + // // "includeEmailInChangeFile": true + // }, + // + // { + // /** + // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). + // * + // * The "individualVersion" mode specifies that the projects will use "individual versioning". + // * This is the typical NPM model where each package has an independent version number + // * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the + // * packages, they otherwise don't have any special relationship. The version bumping will + // * depend on how developers answer the "rush change" questions for each package that + // * is changed. + // */ + // "definitionName": "individualVersion", + // + // "policyName": "MyRandomLibraries", + // + // /** + // * (Optional) This can be used to enforce that all packages in the set must share a common + // * major version number, e.g. because they are from the same major release branch. + // * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes + // * inappropriately. The minor/patch version parts will be bumped independently according + // * to the types of changes made to each project, according to the "rush change" command. + // */ + // "lockedMajor": 3, + // + // /** + // * (Optional) When publishing is managed by Rush, by default the "rush change" command will + // * request changes for any projects that are modified by a pull request. These change entries + // * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates + // * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects + // * belonging to this version policy. + // */ + // "exemptFromRushChange": false, + // + // // "includeEmailInChangeFile": true + // } +] diff --git a/foundations/communication/common/git-hooks/commit-msg.sample b/foundations/communication/common/git-hooks/commit-msg.sample new file mode 100644 index 0000000000..59cacb80ca --- /dev/null +++ b/foundations/communication/common/git-hooks/commit-msg.sample @@ -0,0 +1,25 @@ +#!/bin/sh +# +# This is an example Git hook for use with Rush. To enable this hook, rename this file +# to "commit-msg" and then run "rush install", which will copy it from common/git-hooks +# to the .git/hooks folder. +# +# TO LEARN MORE ABOUT GIT HOOKS +# +# The Git documentation is here: https://git-scm.com/docs/githooks +# Some helpful resources: https://githooks.com +# +# ABOUT THIS EXAMPLE +# +# The commit-msg hook is called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero status after issuing +# an appropriate message if it wants to stop the commit. The hook is allowed to edit +# the commit message file. + +# This example enforces that commit message should contain a minimum amount of +# description text. +if [ `cat $1 | wc -w` -lt 3 ]; then + echo "" + echo "Invalid commit message: The message must contain at least 3 words." + exit 1 +fi diff --git a/foundations/communication/common/scripts/install-run-rush-pnpm.js b/foundations/communication/common/scripts/install-run-rush-pnpm.js new file mode 100644 index 0000000000..2356649f4e --- /dev/null +++ b/foundations/communication/common/scripts/install-run-rush-pnpm.js @@ -0,0 +1,31 @@ +// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. +// +// This script is intended for usage in an automated build environment where the Rush command may not have +// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush +// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the +// rush-pnpm command. +// +// An example usage would be: +// +// node common/scripts/install-run-rush-pnpm.js pnpm-command +// +// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. + +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +var __webpack_exports__ = {}; +/*!*****************************************************!*\ + !*** ./lib-esnext/scripts/install-run-rush-pnpm.js ***! + \*****************************************************/ + +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +require('./install-run-rush'); +//# sourceMappingURL=install-run-rush-pnpm.js.map +module.exports = __webpack_exports__; +/******/ })() +; +//# sourceMappingURL=install-run-rush-pnpm.js.map \ No newline at end of file diff --git a/foundations/communication/common/scripts/install-run-rush.js b/foundations/communication/common/scripts/install-run-rush.js new file mode 100644 index 0000000000..ef1d697f9c --- /dev/null +++ b/foundations/communication/common/scripts/install-run-rush.js @@ -0,0 +1,218 @@ +// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. +// +// This script is intended for usage in an automated build environment where the Rush command may not have +// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush +// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it. +// An example usage would be: +// +// node common/scripts/install-run-rush.js install +// +// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. + +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ({ + +/***/ 16928: +/*!***********************!*\ + !*** external "path" ***! + \***********************/ +/***/ ((module) => { + +module.exports = require("path"); + +/***/ }), + +/***/ 179896: +/*!*********************!*\ + !*** external "fs" ***! + \*********************/ +/***/ ((module) => { + +module.exports = require("fs"); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => (module['default']) : +/******/ () => (module); +/******/ __webpack_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. +(() => { +/*!************************************************!*\ + !*** ./lib-esnext/scripts/install-run-rush.js ***! + \************************************************/ +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 16928); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +/* eslint-disable no-console */ + + +const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run'); +const PACKAGE_NAME = '@microsoft/rush'; +const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION'; +const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH'; +function _getRushVersion(logger) { + const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION]; + if (rushPreviewVersion !== undefined) { + logger.info(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`); + return rushPreviewVersion; + } + const rushJsonFolder = findRushJsonFolder(); + const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME); + try { + const rushJsonContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(rushJsonPath, 'utf-8'); + // Use a regular expression to parse out the rushVersion value because rush.json supports comments, + // but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script. + const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/); + return rushJsonMatches[1]; + } + catch (e) { + throw new Error(`Unable to determine the required version of Rush from ${RUSH_JSON_FILENAME} (${rushJsonFolder}). ` + + `The 'rushVersion' field is either not assigned in ${RUSH_JSON_FILENAME} or was specified ` + + 'using an unexpected syntax.'); + } +} +function _getBin(scriptName) { + switch (scriptName.toLowerCase()) { + case 'install-run-rush-pnpm.js': + return 'rush-pnpm'; + case 'install-run-rushx.js': + return 'rushx'; + default: + return 'rush'; + } +} +function _run() { + const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv; + // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the + // appropriate binary inside the rush package to run + const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath); + const bin = _getBin(scriptName); + if (!nodePath || !scriptPath) { + throw new Error('Unexpected exception: could not detect node path or script path'); + } + let commandFound = false; + let logger = { info: console.log, error: console.error }; + for (const arg of packageBinArgs) { + if (arg === '-q' || arg === '--quiet') { + // The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress + // any normal informational/diagnostic information printed during startup. + // + // To maintain the same user experience, the install-run* scripts pass along this + // flag but also use it to suppress any diagnostic information normally printed + // to stdout. + logger = { + info: () => { }, + error: console.error + }; + } + else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') { + // We either found something that looks like a command (i.e. - doesn't start with a "-"), + // or we found the -h/--help flag, which can be run without a command + commandFound = true; + } + } + if (!commandFound) { + console.log(`Usage: ${scriptName} [args...]`); + if (scriptName === 'install-run-rush-pnpm.js') { + console.log(`Example: ${scriptName} pnpm-command`); + } + else if (scriptName === 'install-run-rush.js') { + console.log(`Example: ${scriptName} build --to myproject`); + } + else { + console.log(`Example: ${scriptName} custom-command`); + } + process.exit(1); + } + runWithErrorAndStatusCode(logger, () => { + const version = _getRushVersion(logger); + logger.info(`The ${RUSH_JSON_FILENAME} configuration requests Rush version ${version}`); + const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE]; + if (lockFilePath) { + logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`); + } + return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath); + }); +} +_run(); +//# sourceMappingURL=install-run-rush.js.map +})(); + +module.exports = __webpack_exports__; +/******/ })() +; +//# sourceMappingURL=install-run-rush.js.map \ No newline at end of file diff --git a/foundations/communication/common/scripts/install-run-rushx.js b/foundations/communication/common/scripts/install-run-rushx.js new file mode 100644 index 0000000000..6581521f3c --- /dev/null +++ b/foundations/communication/common/scripts/install-run-rushx.js @@ -0,0 +1,31 @@ +// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. +// +// This script is intended for usage in an automated build environment where the Rush command may not have +// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush +// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the +// rushx command. +// +// An example usage would be: +// +// node common/scripts/install-run-rushx.js custom-command +// +// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. + +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +var __webpack_exports__ = {}; +/*!*************************************************!*\ + !*** ./lib-esnext/scripts/install-run-rushx.js ***! + \*************************************************/ + +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +require('./install-run-rush'); +//# sourceMappingURL=install-run-rushx.js.map +module.exports = __webpack_exports__; +/******/ })() +; +//# sourceMappingURL=install-run-rushx.js.map \ No newline at end of file diff --git a/foundations/communication/common/scripts/install-run.js b/foundations/communication/common/scripts/install-run.js new file mode 100644 index 0000000000..a35726bf16 --- /dev/null +++ b/foundations/communication/common/scripts/install-run.js @@ -0,0 +1,778 @@ +// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. +// +// This script is intended for usage in an automated build environment where a Node tool may not have +// been preinstalled, or may have an unpredictable version. This script will automatically install the specified +// version of the specified tool (if not already installed), and then pass a command-line to it. +// An example usage would be: +// +// node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io +// +// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. + +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ({ + +/***/ 16928: +/*!***********************!*\ + !*** external "path" ***! + \***********************/ +/***/ ((module) => { + +module.exports = require("path"); + +/***/ }), + +/***/ 179896: +/*!*********************!*\ + !*** external "fs" ***! + \*********************/ +/***/ ((module) => { + +module.exports = require("fs"); + +/***/ }), + +/***/ 370857: +/*!*********************!*\ + !*** external "os" ***! + \*********************/ +/***/ ((module) => { + +module.exports = require("os"); + +/***/ }), + +/***/ 535317: +/*!********************************!*\ + !*** external "child_process" ***! + \********************************/ +/***/ ((module) => { + +module.exports = require("child_process"); + +/***/ }), + +/***/ 832286: +/*!************************************************!*\ + !*** ./lib-esnext/utilities/npmrcUtilities.js ***! + \************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ isVariableSetInNpmrcFile: () => (/* binding */ isVariableSetInNpmrcFile), +/* harmony export */ syncNpmrc: () => (/* binding */ syncNpmrc), +/* harmony export */ trimNpmrcFileLines: () => (/* binding */ trimNpmrcFileLines) +/* harmony export */ }); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ 179896); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! path */ 16928); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +// IMPORTANT - do not use any non-built-in libraries in this file + + +/** + * This function reads the content for given .npmrc file path, and also trims + * unusable lines from the .npmrc file. + * + * @returns + * The text of the the .npmrc. + */ +// create a global _combinedNpmrc for cache purpose +const _combinedNpmrcMap = new Map(); +function _trimNpmrcFile(options) { + const { sourceNpmrcPath, linesToPrepend, linesToAppend, supportEnvVarFallbackSyntax } = options; + const combinedNpmrcFromCache = _combinedNpmrcMap.get(sourceNpmrcPath); + if (combinedNpmrcFromCache !== undefined) { + return combinedNpmrcFromCache; + } + let npmrcFileLines = []; + if (linesToPrepend) { + npmrcFileLines.push(...linesToPrepend); + } + if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { + npmrcFileLines.push(...fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\n')); + } + if (linesToAppend) { + npmrcFileLines.push(...linesToAppend); + } + npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim()); + const resultLines = trimNpmrcFileLines(npmrcFileLines, process.env, supportEnvVarFallbackSyntax); + const combinedNpmrc = resultLines.join('\n'); + //save the cache + _combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc); + return combinedNpmrc; +} +/** + * + * @param npmrcFileLines The npmrc file's lines + * @param env The environment variables object + * @param supportEnvVarFallbackSyntax Whether to support fallback values in the form of `${VAR_NAME:-fallback}` + * @returns + */ +function trimNpmrcFileLines(npmrcFileLines, env, supportEnvVarFallbackSyntax) { + var _a; + const resultLines = []; + // This finds environment variable tokens that look like "${VAR_NAME}" + const expansionRegExp = /\$\{([^\}]+)\}/g; + // Comment lines start with "#" or ";" + const commentRegExp = /^\s*[#;]/; + // Trim out lines that reference environment variables that aren't defined + for (let line of npmrcFileLines) { + let lineShouldBeTrimmed = false; + //remove spaces before or after key and value + line = line + .split('=') + .map((lineToTrim) => lineToTrim.trim()) + .join('='); + // Ignore comment lines + if (!commentRegExp.test(line)) { + const environmentVariables = line.match(expansionRegExp); + if (environmentVariables) { + for (const token of environmentVariables) { + /** + * Remove the leading "${" and the trailing "}" from the token + * + * ${nameString} -> nameString + * ${nameString-fallbackString} -> name-fallbackString + * ${nameString:-fallbackString} -> name:-fallbackString + */ + const nameWithFallback = token.substring(2, token.length - 1); + let environmentVariableName; + let fallback; + if (supportEnvVarFallbackSyntax) { + /** + * Get the environment variable name and fallback value. + * + * name fallback + * nameString -> nameString undefined + * nameString-fallbackString -> nameString fallbackString + * nameString:-fallbackString -> nameString fallbackString + */ + const matched = nameWithFallback.match(/^([^:-]+)(?:\:?-(.+))?$/); + // matched: [originStr, variableName, fallback] + environmentVariableName = (_a = matched === null || matched === void 0 ? void 0 : matched[1]) !== null && _a !== void 0 ? _a : nameWithFallback; + fallback = matched === null || matched === void 0 ? void 0 : matched[2]; + } + else { + environmentVariableName = nameWithFallback; + } + // Is the environment variable and fallback value defined. + if (!env[environmentVariableName] && !fallback) { + // No, so trim this line + lineShouldBeTrimmed = true; + break; + } + } + } + } + if (lineShouldBeTrimmed) { + // Example output: + // "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}" + resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line); + } + else { + resultLines.push(line); + } + } + return resultLines; +} +function _copyAndTrimNpmrcFile(options) { + const { logger, sourceNpmrcPath, targetNpmrcPath } = options; + logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose + logger.info(` --> "${targetNpmrcPath}"`); + const combinedNpmrc = _trimNpmrcFile(options); + fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(targetNpmrcPath, combinedNpmrc); + return combinedNpmrc; +} +function syncNpmrc(options) { + const { sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger = { + // eslint-disable-next-line no-console + info: console.log, + // eslint-disable-next-line no-console + error: console.error + }, createIfMissing = false } = options; + const sourceNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish'); + const targetNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(targetNpmrcFolder, '.npmrc'); + try { + if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath) || createIfMissing) { + // Ensure the target folder exists + if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcFolder)) { + fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(targetNpmrcFolder, { recursive: true }); + } + return _copyAndTrimNpmrcFile({ + sourceNpmrcPath, + targetNpmrcPath, + logger, + ...options + }); + } + else if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcPath)) { + // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target + logger.info(`Deleting ${targetNpmrcPath}`); // Verbose + fs__WEBPACK_IMPORTED_MODULE_0__.unlinkSync(targetNpmrcPath); + } + } + catch (e) { + throw new Error(`Error syncing .npmrc file: ${e}`); + } +} +function isVariableSetInNpmrcFile(sourceNpmrcFolder, variableKey, supportEnvVarFallbackSyntax) { + const sourceNpmrcPath = `${sourceNpmrcFolder}/.npmrc`; + //if .npmrc file does not exist, return false directly + if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { + return false; + } + const trimmedNpmrcFile = _trimNpmrcFile({ sourceNpmrcPath, supportEnvVarFallbackSyntax }); + const variableKeyRegExp = new RegExp(`^${variableKey}=`, 'm'); + return trimmedNpmrcFile.match(variableKeyRegExp) !== null; +} +//# sourceMappingURL=npmrcUtilities.js.map + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => (module['default']) : +/******/ () => (module); +/******/ __webpack_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. +(() => { +/*!*******************************************!*\ + !*** ./lib-esnext/scripts/install-run.js ***! + \*******************************************/ +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ RUSH_JSON_FILENAME: () => (/* binding */ RUSH_JSON_FILENAME), +/* harmony export */ findRushJsonFolder: () => (/* binding */ findRushJsonFolder), +/* harmony export */ getNpmPath: () => (/* binding */ getNpmPath), +/* harmony export */ installAndRun: () => (/* binding */ installAndRun), +/* harmony export */ runWithErrorAndStatusCode: () => (/* binding */ runWithErrorAndStatusCode) +/* harmony export */ }); +/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! child_process */ 535317); +/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(child_process__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! os */ 370857); +/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! path */ 16928); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); +/* harmony import */ var _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utilities/npmrcUtilities */ 832286); +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +/* eslint-disable no-console */ + + + + + +const RUSH_JSON_FILENAME = 'rush.json'; +const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER'; +const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH'; +const INSTALLED_FLAG_FILENAME = 'installed.flag'; +const NODE_MODULES_FOLDER_NAME = 'node_modules'; +const PACKAGE_JSON_FILENAME = 'package.json'; +/** + * Parse a package specifier (in the form of name\@version) into name and version parts. + */ +function _parsePackageSpecifier(rawPackageSpecifier) { + rawPackageSpecifier = (rawPackageSpecifier || '').trim(); + const separatorIndex = rawPackageSpecifier.lastIndexOf('@'); + let name; + let version = undefined; + if (separatorIndex === 0) { + // The specifier starts with a scope and doesn't have a version specified + name = rawPackageSpecifier; + } + else if (separatorIndex === -1) { + // The specifier doesn't have a version + name = rawPackageSpecifier; + } + else { + name = rawPackageSpecifier.substring(0, separatorIndex); + version = rawPackageSpecifier.substring(separatorIndex + 1); + } + if (!name) { + throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`); + } + return { name, version }; +} +let _npmPath = undefined; +/** + * Get the absolute path to the npm executable + */ +function getNpmPath() { + if (!_npmPath) { + try { + if (_isWindows()) { + // We're on Windows + const whereOutput = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('where npm', { stdio: [] }).toString(); + const lines = whereOutput.split(os__WEBPACK_IMPORTED_MODULE_2__.EOL).filter((line) => !!line); + // take the last result, we are looking for a .cmd command + // see https://github.com/microsoft/rushstack/issues/759 + _npmPath = lines[lines.length - 1]; + } + else { + // We aren't on Windows - assume we're on *NIX or Darwin + _npmPath = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('command -v npm', { stdio: [] }).toString(); + } + } + catch (e) { + throw new Error(`Unable to determine the path to the NPM tool: ${e}`); + } + _npmPath = _npmPath.trim(); + if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(_npmPath)) { + throw new Error('The NPM executable does not exist'); + } + } + return _npmPath; +} +function _ensureFolder(folderPath) { + if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(folderPath)) { + const parentDir = path__WEBPACK_IMPORTED_MODULE_3__.dirname(folderPath); + _ensureFolder(parentDir); + fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(folderPath); + } +} +/** + * Create missing directories under the specified base directory, and return the resolved directory. + * + * Does not support "." or ".." path segments. + * Assumes the baseFolder exists. + */ +function _ensureAndJoinPath(baseFolder, ...pathSegments) { + let joinedPath = baseFolder; + try { + for (let pathSegment of pathSegments) { + pathSegment = pathSegment.replace(/[\\\/]/g, '+'); + joinedPath = path__WEBPACK_IMPORTED_MODULE_3__.join(joinedPath, pathSegment); + if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(joinedPath)) { + fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(joinedPath); + } + } + } + catch (e) { + throw new Error(`Error building local installation folder (${path__WEBPACK_IMPORTED_MODULE_3__.join(baseFolder, ...pathSegments)}): ${e}`); + } + return joinedPath; +} +function _getRushTempFolder(rushCommonFolder) { + const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME]; + if (rushTempFolder !== undefined) { + _ensureFolder(rushTempFolder); + return rushTempFolder; + } + else { + return _ensureAndJoinPath(rushCommonFolder, 'temp'); + } +} +/** + * Compare version strings according to semantic versioning. + * Returns a positive integer if "a" is a later version than "b", + * a negative integer if "b" is later than "a", + * and 0 otherwise. + */ +function _compareVersionStrings(a, b) { + const aParts = a.split(/[.-]/); + const bParts = b.split(/[.-]/); + const numberOfParts = Math.max(aParts.length, bParts.length); + for (let i = 0; i < numberOfParts; i++) { + if (aParts[i] !== bParts[i]) { + return (Number(aParts[i]) || 0) - (Number(bParts[i]) || 0); + } + } + return 0; +} +/** + * Resolve a package specifier to a static version + */ +function _resolvePackageVersion(logger, rushCommonFolder, { name, version }) { + if (!version) { + version = '*'; // If no version is specified, use the latest version + } + if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) { + // If the version contains only characters that we recognize to be used in static version specifiers, + // pass the version through + return version; + } + else { + // version resolves to + try { + const rushTempFolder = _getRushTempFolder(rushCommonFolder); + const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); + (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ + sourceNpmrcFolder, + targetNpmrcFolder: rushTempFolder, + logger, + supportEnvVarFallbackSyntax: false + }); + const npmPath = getNpmPath(); + // This returns something that looks like: + // ``` + // [ + // "3.0.0", + // "3.0.1", + // ... + // "3.0.20" + // ] + // ``` + // + // if multiple versions match the selector, or + // + // ``` + // "3.0.0" + // ``` + // + // if only a single version matches. + const spawnSyncOptions = { + cwd: rushTempFolder, + stdio: [], + shell: _isWindows() + }; + const platformNpmPath = _getPlatformPath(npmPath); + const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'], spawnSyncOptions); + if (npmVersionSpawnResult.status !== 0) { + throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`); + } + const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString(); + const parsedVersionOutput = JSON.parse(npmViewVersionOutput); + const versions = Array.isArray(parsedVersionOutput) + ? parsedVersionOutput + : [parsedVersionOutput]; + let latestVersion = versions[0]; + for (let i = 1; i < versions.length; i++) { + const latestVersionCandidate = versions[i]; + if (_compareVersionStrings(latestVersionCandidate, latestVersion) > 0) { + latestVersion = latestVersionCandidate; + } + } + if (!latestVersion) { + throw new Error('No versions found for the specified version range.'); + } + return latestVersion; + } + catch (e) { + throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`); + } + } +} +let _rushJsonFolder; +/** + * Find the absolute path to the folder containing rush.json + */ +function findRushJsonFolder() { + if (!_rushJsonFolder) { + let basePath = __dirname; + let tempPath = __dirname; + do { + const testRushJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(basePath, RUSH_JSON_FILENAME); + if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(testRushJsonPath)) { + _rushJsonFolder = basePath; + break; + } + else { + basePath = tempPath; + } + } while (basePath !== (tempPath = path__WEBPACK_IMPORTED_MODULE_3__.dirname(basePath))); // Exit the loop when we hit the disk root + if (!_rushJsonFolder) { + throw new Error(`Unable to find ${RUSH_JSON_FILENAME}.`); + } + } + return _rushJsonFolder; +} +/** + * Detects if the package in the specified directory is installed + */ +function _isPackageAlreadyInstalled(packageInstallFolder) { + try { + const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); + if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(flagFilePath)) { + return false; + } + const fileContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(flagFilePath).toString(); + return fileContents.trim() === process.version; + } + catch (e) { + return false; + } +} +/** + * Delete a file. Fail silently if it does not exist. + */ +function _deleteFile(file) { + try { + fs__WEBPACK_IMPORTED_MODULE_1__.unlinkSync(file); + } + catch (err) { + if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') { + throw err; + } + } +} +/** + * Removes the following files and directories under the specified folder path: + * - installed.flag + * - + * - node_modules + */ +function _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) { + try { + const flagFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME); + _deleteFile(flagFile); + const packageLockFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, 'package-lock.json'); + if (lockFilePath) { + fs__WEBPACK_IMPORTED_MODULE_1__.copyFileSync(lockFilePath, packageLockFile); + } + else { + // Not running `npm ci`, so need to cleanup + _deleteFile(packageLockFile); + const nodeModulesFolder = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME); + if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(nodeModulesFolder)) { + const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler'); + fs__WEBPACK_IMPORTED_MODULE_1__.renameSync(nodeModulesFolder, path__WEBPACK_IMPORTED_MODULE_3__.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`)); + } + } + } + catch (e) { + throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`); + } +} +function _createPackageJson(packageInstallFolder, name, version) { + try { + const packageJsonContents = { + name: 'ci-rush', + version: '0.0.0', + dependencies: { + [name]: version + }, + description: "DON'T WARN", + repository: "DON'T WARN", + license: 'MIT' + }; + const packageJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, PACKAGE_JSON_FILENAME); + fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2)); + } + catch (e) { + throw new Error(`Unable to create package.json: ${e}`); + } +} +/** + * Run "npm install" in the package install folder. + */ +function _installPackage(logger, packageInstallFolder, name, version, command) { + try { + logger.info(`Installing ${name}...`); + const npmPath = getNpmPath(); + const platformNpmPath = _getPlatformPath(npmPath); + const result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, [command], { + stdio: 'inherit', + cwd: packageInstallFolder, + env: process.env, + shell: _isWindows() + }); + if (result.status !== 0) { + throw new Error(`"npm ${command}" encountered an error`); + } + logger.info(`Successfully installed ${name}@${version}`); + } + catch (e) { + throw new Error(`Unable to install package: ${e}`); + } +} +/** + * Get the ".bin" path for the package. + */ +function _getBinPath(packageInstallFolder, binName) { + const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); + const resolvedBinName = _isWindows() ? `${binName}.cmd` : binName; + return path__WEBPACK_IMPORTED_MODULE_3__.resolve(binFolderPath, resolvedBinName); +} +/** + * Returns a cross-platform path - windows must enclose any path containing spaces within double quotes. + */ +function _getPlatformPath(platformPath) { + return _isWindows() && platformPath.includes(' ') ? `"${platformPath}"` : platformPath; +} +function _isWindows() { + return os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32'; +} +/** + * Write a flag file to the package's install directory, signifying that the install was successful. + */ +function _writeFlagFile(packageInstallFolder) { + try { + const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); + fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(flagFilePath, process.version); + } + catch (e) { + throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`); + } +} +function installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs, lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE]) { + const rushJsonFolder = findRushJsonFolder(); + const rushCommonFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushJsonFolder, 'common'); + const rushTempFolder = _getRushTempFolder(rushCommonFolder); + const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`); + if (!_isPackageAlreadyInstalled(packageInstallFolder)) { + // The package isn't already installed + _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath); + const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); + (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ + sourceNpmrcFolder, + targetNpmrcFolder: packageInstallFolder, + logger, + supportEnvVarFallbackSyntax: false + }); + _createPackageJson(packageInstallFolder, packageName, packageVersion); + const command = lockFilePath ? 'ci' : 'install'; + _installPackage(logger, packageInstallFolder, packageName, packageVersion, command); + _writeFlagFile(packageInstallFolder); + } + const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`; + const statusMessageLine = new Array(statusMessage.length + 1).join('-'); + logger.info('\n' + statusMessage + '\n' + statusMessageLine + '\n'); + const binPath = _getBinPath(packageInstallFolder, packageBinName); + const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); + // Windows environment variables are case-insensitive. Instead of using SpawnSyncOptions.env, we need to + // assign via the process.env proxy to ensure that we append to the right PATH key. + const originalEnvPath = process.env.PATH || ''; + let result; + try { + // `npm` bin stubs on Windows are `.cmd` files + // Node.js will not directly invoke a `.cmd` file unless `shell` is set to `true` + const platformBinPath = _getPlatformPath(binPath); + process.env.PATH = [binFolderPath, originalEnvPath].join(path__WEBPACK_IMPORTED_MODULE_3__.delimiter); + result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformBinPath, packageBinArgs, { + stdio: 'inherit', + windowsVerbatimArguments: false, + shell: _isWindows(), + cwd: process.cwd(), + env: process.env + }); + } + finally { + process.env.PATH = originalEnvPath; + } + if (result.status !== null) { + return result.status; + } + else { + throw result.error || new Error('An unknown error occurred.'); + } +} +function runWithErrorAndStatusCode(logger, fn) { + process.exitCode = 1; + try { + const exitCode = fn(); + process.exitCode = exitCode; + } + catch (e) { + logger.error('\n\n' + e.toString() + '\n\n'); + } +} +function _run() { + const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, rawPackageSpecifier /* qrcode@^1.2.0 */, packageBinName /* qrcode */, ...packageBinArgs /* [-f, myproject/lib] */] = process.argv; + if (!nodePath) { + throw new Error('Unexpected exception: could not detect node path'); + } + if (path__WEBPACK_IMPORTED_MODULE_3__.basename(scriptPath).toLowerCase() !== 'install-run.js') { + // If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control + // to the script that (presumably) imported this file + return; + } + if (process.argv.length < 4) { + console.log('Usage: install-run.js @ [args...]'); + console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io'); + process.exit(1); + } + const logger = { info: console.log, error: console.error }; + runWithErrorAndStatusCode(logger, () => { + const rushJsonFolder = findRushJsonFolder(); + const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common'); + const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier); + const name = packageSpecifier.name; + const version = _resolvePackageVersion(logger, rushCommonFolder, packageSpecifier); + if (packageSpecifier.version !== version) { + console.log(`Resolved to ${name}@${version}`); + } + return installAndRun(logger, name, version, packageBinName, packageBinArgs); + }); +} +_run(); +//# sourceMappingURL=install-run.js.map +})(); + +module.exports = __webpack_exports__; +/******/ })() +; +//# sourceMappingURL=install-run.js.map \ No newline at end of file diff --git a/foundations/communication/common/scripts/package.json b/foundations/communication/common/scripts/package.json new file mode 100644 index 0000000000..bb9cc54cda --- /dev/null +++ b/foundations/communication/common/scripts/package.json @@ -0,0 +1,19 @@ +{ + "name": "@hcengineering/scripts", + "version": "0.7.11", + "scripts": { + "format": "echo \"No format specified\"" + }, + "devDependencies": { + "esbuild": "^0.25.10", + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0" + }, + "private": true +} diff --git a/foundations/communication/common/scripts/run-tests-with-coverage.js b/foundations/communication/common/scripts/run-tests-with-coverage.js new file mode 100755 index 0000000000..150cd21ff7 --- /dev/null +++ b/foundations/communication/common/scripts/run-tests-with-coverage.js @@ -0,0 +1,116 @@ +#!/usr/bin/env node + +/** + * Run tests with coverage for all packages + * This is a replacement for show-coverage.sh + */ + +const fs = require('fs') +const path = require('path') +const { spawn } = require('child_process') + +const root = process.cwd() +const packagesDir = path.join(root, 'packages') + +if (!fs.existsSync(packagesDir)) { + console.error('Error: packages directory not found') + process.exit(1) +} + +console.log('=== RUNNING TESTS WITH COVERAGE ===') +console.log('') + +const packages = fs + .readdirSync(packagesDir, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name) + .sort() + +async function runTestsForPackage(pkgName) { + const pkgDir = path.join(packagesDir, pkgName) + const packageJson = path.join(pkgDir, 'package.json') + + if (!fs.existsSync(packageJson)) { + return null + } + + const pkg = JSON.parse(fs.readFileSync(packageJson, 'utf8')) + if (!pkg.scripts || !pkg.scripts.test) { + return null + } + + console.log(`📦 Package: ${pkgName}`) + console.log('---') + + return new Promise((resolve) => { + const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm' + const child = spawn(npm, ['test', '--', '--coverage', '--silent'], { + cwd: pkgDir, + stdio: 'pipe', + shell: true + }) + + let output = '' + let inSummary = false + + child.stdout.on('data', (data) => { + const text = data.toString() + output += text + + // Extract coverage summary + const lines = text.split('\n') + for (const line of lines) { + if (line.includes('Coverage summary') || line.includes('----------')) { + inSummary = true + } + if ( + inSummary && + (line.includes('Statements') || + line.includes('Branches') || + line.includes('Functions') || + line.includes('Lines')) + ) { + console.log(line.trim()) + } + if (inSummary && line.trim() === '') { + inSummary = false + } + } + }) + + child.stderr.on('data', (data) => { + // Ignore stderr for cleaner output + }) + + child.on('close', (code) => { + console.log('') + resolve({ package: pkgName, code, output }) + }) + }) +} + +async function main() { + const results = [] + + for (const pkg of packages) { + const result = await runTestsForPackage(pkg) + if (result) { + results.push(result) + } + } + + console.log('=== END OF COVERAGE REPORT ===') + console.log('') + console.log(`Tested ${results.length} packages`) + + const failed = results.filter((r) => r.code !== 0) + if (failed.length > 0) { + console.log(`⚠️ ${failed.length} package(s) had test failures:`) + failed.forEach((r) => console.log(` - ${r.package}`)) + } +} + +main().catch((err) => { + console.error('Error:', err) + process.exit(1) +}) diff --git a/foundations/communication/common/scripts/show-coverage-summary.js b/foundations/communication/common/scripts/show-coverage-summary.js new file mode 100755 index 0000000000..f2ca9e98b8 --- /dev/null +++ b/foundations/communication/common/scripts/show-coverage-summary.js @@ -0,0 +1,117 @@ +#!/usr/bin/env node + +/** + * Display coverage summary from lcov.info file + * Usage: node show-coverage-summary.js [lcov-file-path] + */ + +const fs = require('fs') +const path = require('path') + +const lcovFile = process.argv[2] || 'coverage/lcov.info' +const root = process.cwd() +const lcovPath = path.isAbsolute(lcovFile) ? lcovFile : path.join(root, lcovFile) + +if (!fs.existsSync(lcovPath)) { + console.error(`Error: LCOV file not found: ${lcovPath}`) + process.exit(1) +} + +console.log('==============================================') +console.log('COVERAGE SUMMARY BY PACKAGE') +console.log('==============================================') +console.log('') + +const data = fs.readFileSync(lcovPath, 'utf8') +const lines = data.split(/\r?\n/) + +const fileStats = new Map() +const filePkg = new Map() +let currentFile = '' + +// Parse lcov.info +for (const line of lines) { + if (line.startsWith('SF:')) { + currentFile = line.substring(3) + fileStats.set(currentFile, { total: 0, covered: 0 }) + + // Extract package name from path + const parts = currentFile.split(path.sep) + const pkgIndex = parts.indexOf('packages') + if (pkgIndex !== -1 && pkgIndex + 1 < parts.length) { + filePkg.set(currentFile, parts[pkgIndex + 1]) + } + } else if (line.startsWith('DA:')) { + const [lineNum, hitCount] = line.substring(3).split(',') + const stats = fileStats.get(currentFile) + if (stats) { + stats.total++ + if (parseInt(hitCount) > 0) { + stats.covered++ + } + } + } +} + +// Aggregate by package +const pkgStats = new Map() +for (const [file, stats] of fileStats) { + const pkg = filePkg.get(file) + if (pkg) { + if (!pkgStats.has(pkg)) { + pkgStats.set(pkg, { total: 0, covered: 0 }) + } + const pkgStat = pkgStats.get(pkg) + pkgStat.total += stats.total + pkgStat.covered += stats.covered + } +} + +// Sort packages alphabetically +const sortedPackages = Array.from(pkgStats.keys()).sort() + +// Display header +console.log(padRight('Package', 25) + padLeft('Covered', 10) + padLeft('Total', 10) + padLeft('Coverage', 10)) +console.log('----------------------------------------------') + +// Display package stats +let overallCovered = 0 +let overallTotal = 0 + +for (const pkg of sortedPackages) { + const stats = pkgStats.get(pkg) + const pct = (stats.covered / stats.total) * 100 + console.log( + padRight(pkg, 25) + + padLeft(stats.covered.toString(), 10) + + padLeft(stats.total.toString(), 10) + + padLeft(pct.toFixed(2) + '%', 10) + ) + overallCovered += stats.covered + overallTotal += stats.total +} + +// Display total +console.log('----------------------------------------------') +const overallPct = (overallCovered / overallTotal) * 100 +console.log( + padRight('TOTAL', 25) + + padLeft(overallCovered.toString(), 10) + + padLeft(overallTotal.toString(), 10) + + padLeft(overallPct.toFixed(2) + '%', 10) +) +console.log('') + +console.log('==============================================') +console.log('') +console.log('HTML report available at: coverage/html/index.html') +console.log(`Merged LCOV file available at: ${lcovFile}`) + +// Helper functions +function padRight(str, width) { + return str + ' '.repeat(Math.max(0, width - str.length)) +} + +function padLeft(str, width) { + return ' '.repeat(Math.max(0, width - str.length)) + str +} diff --git a/foundations/communication/common/scripts/show-coverage-summary.sh b/foundations/communication/common/scripts/show-coverage-summary.sh new file mode 100755 index 0000000000..7ae68f8391 --- /dev/null +++ b/foundations/communication/common/scripts/show-coverage-summary.sh @@ -0,0 +1,76 @@ +git#!/bin/bash + +# Script to display coverage summary from lcov.info file + +LCOV_FILE="${1:-coverage/lcov.info}" + +if [ ! -f "$LCOV_FILE" ]; then + echo "Error: LCOV file not found: $LCOV_FILE" + exit 1 +fi + +echo "==============================================" +echo "COVERAGE SUMMARY BY PACKAGE" +echo "==============================================" +echo "" + +# Parse lcov.info and aggregate by package +awk ' +BEGIN { + current_file = ""; +} +/^SF:/ { + current_file = $0; + sub(/^SF:/, "", current_file); + # Extract package name from path + split(current_file, parts, "/"); + pkg = ""; + for (i=1; i<=length(parts); i++) { + if (parts[i] == "packages" && i+1 <= length(parts)) { + pkg = parts[i+1]; + break; + } + } + total_lines[current_file] = 0; + covered_lines[current_file] = 0; + file_pkg[current_file] = pkg; +} +/^DA:/ { + split($0, parts, ","); + total_lines[current_file]++; + if (parts[2] > 0) covered_lines[current_file]++; +} +END { + # Aggregate by package + for (file in total_lines) { + pkg = file_pkg[file]; + if (pkg != "") { + pkg_total[pkg] += total_lines[file]; + pkg_covered[pkg] += covered_lines[file]; + } + } + + printf "%-25s %10s %10s %10s\n", "Package", "Covered", "Total", "Coverage"; + print "----------------------------------------------"; + + overall_covered = 0; + overall_total = 0; + + # Display packages + for (pkg in pkg_total) { + pct = (pkg_covered[pkg] / pkg_total[pkg]) * 100; + printf "%-25s %10d %10d %9.2f%%\n", pkg, pkg_covered[pkg], pkg_total[pkg], pct; + overall_covered += pkg_covered[pkg]; + overall_total += pkg_total[pkg]; + } + + print "----------------------------------------------"; + overall_pct = (overall_covered / overall_total) * 100; + printf "%-25s %10d %10d %9.2f%%\n", "TOTAL", overall_covered, overall_total, overall_pct; + print ""; +}' "$LCOV_FILE" + +echo "==============================================" +echo "" +echo "HTML report available at: coverage/html/index.html" +echo "Merged LCOV file available at: $LCOV_FILE" diff --git a/foundations/communication/common/scripts/show-coverage.sh b/foundations/communication/common/scripts/show-coverage.sh new file mode 100755 index 0000000000..cc99b27499 --- /dev/null +++ b/foundations/communication/common/scripts/show-coverage.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +echo "=== FINAL COVERAGE REPORT ===" +echo "" + +# Iterate through each package directory +for pkg in packages/*/; do + pkgname=$(basename "$pkg") + + echo "📦 Package: $pkgname" + echo "---" + + # Change to package directory + cd "$pkg" || continue + + # Run tests with coverage and extract summary + npm test -- --coverage --silent 2>&1 | \ + grep -A 4 "Coverage summary" | \ + grep -E "Statements|Branches|Functions|Lines" + + # Return to root directory + cd ../.. || exit + + echo "" +done + +echo "=== END OF COVERAGE REPORT ===" \ No newline at end of file diff --git a/foundations/communication/example.json b/foundations/communication/example.json new file mode 100644 index 0000000000..ac066a5e22 --- /dev/null +++ b/foundations/communication/example.json @@ -0,0 +1,78 @@ +{ + "cardId": "card-000", + "fromDate": "2025-09-02T10:00:00.000Z", + "toDate": "2025-09-02T12:30:00.000Z", + "language": "original", + + "messages": { + "msg-001": { + "id": "msg-001", + "cardId": "card-000", + "created": "2025-09-02T10:00:00.000Z", + "creator": "user-456", + "type": "message", + "content": "Привет! Это первое сообщение.", + "extra": {}, + "modified": "2025-09-02T12:00:00.000Z", + "reactions": { + "👍": { + "anonymous": { + "count": 100, + "created": "2025-09-02T12:00:00.000Z" + }, + "user-222": { + "count": 1, + "created": "2025-09-02T12:00:00.000Z" + }, + "user-333": { + "count": 1, + "created": "2025-09-02T12:00:00.000Z" + } + } + }, + "attachments": { + "att-001": { + "id": "att-001", + "mimeType": "image/png", + "params": { + "blobId": "blob-001", + "fileName": "image.png", + "size": 1024, + "metadata": {} + }, + "creator": "user-111", + "created": "2025-09-02T12:00:00.000Z" + }, + "att-002": { + "id": "att-002", + "mimeType": "image/png", + "params": { + "blobId": "blob-002", + "fileName": "image.png", + "size": 1024, + "metadata": {} + }, + "creator": "user-111", + "created": "2025-09-02T12:00:00.000Z" + } + }, + "threads": { + "thread-001": { + "threadId": "thread-001", + "repliesCount": 0, + "lastReply": null, + "repliedPersons": {} + }, + "thread-002": { + "threadId": "thread-002", + "repliesCount": 1, + "lastReply": "2025-09-02T12:30:00.000Z", + "repliedPersons": { + "user-222": 1 + } + } + } + } + } +} + diff --git a/foundations/communication/packages/client-query/.eslintrc.cjs b/foundations/communication/packages/client-query/.eslintrc.cjs new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/communication/packages/client-query/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/communication/packages/client-query/CHANGELOG.json b/foundations/communication/packages/client-query/CHANGELOG.json new file mode 100644 index 0000000000..d93647f948 --- /dev/null +++ b/foundations/communication/packages/client-query/CHANGELOG.json @@ -0,0 +1,61 @@ +{ + "name": "@hcengineering/communication-client-query", + "entries": [ + { + "version": "0.7.11", + "tag": "@hcengineering/communication-client-query_v0.7.11", + "date": "Mon, 27 Oct 2025 16:28:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.11` to `0.7.12`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.11` to `0.7.12`" + } + ] + } + }, + { + "version": "0.7.8", + "tag": "@hcengineering/communication-client-query_v0.7.8", + "date": "Sat, 25 Oct 2025 22:20:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.8` to `0.7.9`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.8` to `0.7.9`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/communication-client-query_v0.7.4", + "date": "Tue, 14 Oct 2025 10:12:38 GMT", + "comments": { + "patch": [ + { + "comment": "update hulylake client" + }, + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-query\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/communication/packages/client-query/CHANGELOG.md b/foundations/communication/packages/client-query/CHANGELOG.md new file mode 100644 index 0000000000..39021f5c31 --- /dev/null +++ b/foundations/communication/packages/client-query/CHANGELOG.md @@ -0,0 +1,22 @@ +# Change Log - @hcengineering/communication-client-query + +This log was last generated on Mon, 27 Oct 2025 16:28:25 GMT and should not be manually modified. + +## 0.7.11 +Mon, 27 Oct 2025 16:28:25 GMT + +_Version update only_ + +## 0.7.8 +Sat, 25 Oct 2025 22:20:35 GMT + +_Version update only_ + +## 0.7.4 +Tue, 14 Oct 2025 10:12:38 GMT + +### Patches + +- update hulylake client +- update deps + diff --git a/foundations/communication/packages/client-query/config/rig.json b/foundations/communication/packages/client-query/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/communication/packages/client-query/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/communication/packages/client-query/package.json b/foundations/communication/packages/client-query/package.json new file mode 100644 index 0000000000..7a63da4b3a --- /dev/null +++ b/foundations/communication/packages/client-query/package.json @@ -0,0 +1,59 @@ +{ + "name": "@hcengineering/communication-client-query", + "version": "0.7.11", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "tsconfig.json" + ], + "scripts": { + "build": "compile", + "_phase:build": "compile transpile src", + "_phase:validate": "compile validate", + "lint": "eslint \"src/**/*.ts\"", + "lint:fix": "eslint --fix \"src/**/*.ts\"", + "format": "prettier --write src/**/*.ts && eslint --fix \"src/**/*.ts\" && echo 'Formatted'", + "clean": "rm -rf lib && rm -rf types rm -rf node_modules" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@types/jest": "^29.5.5", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "esbuild": "^0.25.10", + "esbuild-plugin-copy": "^2.1.1", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3" + }, + "dependencies": { + "@hcengineering/communication-types": "workspace:^0.7.12", + "@hcengineering/communication-sdk-types": "workspace:^0.7.12", + "@hcengineering/communication-query": "workspace:^0.7.11", + "@hcengineering/hulylake-client": "workspace:^0.7.17", + "fast-equals": "^5.2.2" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hcengineering/communication.git" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/communication/packages/client-query/src/index.ts b/foundations/communication/packages/client-query/src/index.ts new file mode 100644 index 0000000000..45fd04a344 --- /dev/null +++ b/foundations/communication/packages/client-query/src/index.ts @@ -0,0 +1,39 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { CollaboratorsQuery, LabelsQuery, MessagesQuery, NotificationContextsQuery, NotificationsQuery } from './query' + +export type { MessageQueryParams } from '@hcengineering/communication-query' +export { initLiveQueries, refreshLiveQueries, closeLiveQueries } from './init' + +export function createMessagesQuery (dontDestroy?: boolean): MessagesQuery { + return new MessagesQuery(dontDestroy) +} + +export function createNotificationsQuery (dontDestroy?: boolean): NotificationsQuery { + return new NotificationsQuery(dontDestroy) +} + +export function createNotificationContextsQuery (dontDestroy?: boolean): NotificationContextsQuery { + return new NotificationContextsQuery(dontDestroy) +} + +export function createLabelsQuery (dontDestroy?: boolean): LabelsQuery { + return new LabelsQuery(dontDestroy) +} + +export function createCollaboratorsQuery (dontDestroy?: boolean): CollaboratorsQuery { + return new CollaboratorsQuery(dontDestroy) +} diff --git a/foundations/communication/packages/client-query/src/init.ts b/foundations/communication/packages/client-query/src/init.ts new file mode 100644 index 0000000000..f7e47affa0 --- /dev/null +++ b/foundations/communication/packages/client-query/src/init.ts @@ -0,0 +1,58 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { LiveQueries } from '@hcengineering/communication-query' +import type { FindClient } from '@hcengineering/communication-sdk-types' +import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client' + +export type { MessageQueryParams } from '@hcengineering/communication-query' + +let lq: LiveQueries +let onDestroy: (fn: () => void) => void = () => {} + +export function getLiveQueries (): LiveQueries { + return lq +} + +export function getOnDestroy (): (fn: () => void) => void { + return onDestroy +} + +export function initLiveQueries ( + client: FindClient, + hulylake: HulylakeWorkspaceClient, + destroyFn?: (fn: () => void) => void +): void { + if (lq != null) { + lq.close() + } + + if (destroyFn != null) { + onDestroy = destroyFn + } + + lq = new LiveQueries(client, hulylake) +} + +export async function refreshLiveQueries (): Promise { + if (lq != null) { + await lq.refresh() + } +} +export function closeLiveQueries (): void { + if (lq != null) { + lq.close() + } +} diff --git a/foundations/communication/packages/client-query/src/query.ts b/foundations/communication/packages/client-query/src/query.ts new file mode 100644 index 0000000000..c2be11c5c0 --- /dev/null +++ b/foundations/communication/packages/client-query/src/query.ts @@ -0,0 +1,154 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + MessageQueryParams, + NotificationContextQueryOptions, + NotificationQueryParams, + MessageQueryOptions, + QueryOptions +} from '@hcengineering/communication-query' +import type { PagedQueryCallback, QueryCallback } from '@hcengineering/communication-sdk-types' +import { + type FindLabelsParams, + type FindNotificationContextParams, + type Label, + type Message, + type NotificationContext, + type Notification, + FindCollaboratorsParams, + Collaborator +} from '@hcengineering/communication-types' +import { deepEqual } from 'fast-equals' +import { getLiveQueries, getOnDestroy } from './init' + +class BaseQuery

, C extends (r: any) => void, O extends QueryOptions = QueryOptions> { + private oldParams: P | undefined + private oldOptions: O | undefined + private oldCallback: C | undefined + + constructor (dontDestroy?: boolean) { + if (dontDestroy !== true) { + const destroyFn = getOnDestroy() + destroyFn(() => { + this.unsubscribe() + }) + } + } + + private _unsubscribe: (isUpdate: boolean) => void = () => {} + + public unsubscribe (): void { + this._unsubscribe(false) + } + + query (params: P, callback: C, options?: O): boolean { + if (!this.needUpdate(params, callback, options)) { + return false + } + this.doQuery(params, callback, options) + return true + } + + private doQuery (params: P, callback: C, options?: O): void { + const isUpdate = this.oldParams !== undefined + this._unsubscribe(isUpdate) + this.oldCallback = callback + this.oldParams = params + this.oldOptions = options + + const { unsubscribe } = this.createQuery(params, callback, options) + this._unsubscribe = (isUpdate) => { + unsubscribe(isUpdate) + this.oldCallback = undefined + this.oldParams = undefined + this.oldOptions = undefined + this._unsubscribe = () => {} + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + createQuery (params: P, callback: C, options?: O): { unsubscribe: (isUpdate: boolean) => void } { + return { + unsubscribe: () => {} + } + } + + private needUpdate (params: P, callback: C, options?: O): boolean { + if (!deepEqual(params, this.oldParams)) return true + if (!deepEqual(options, this.oldOptions)) return true + if (!deepEqual(callback.toString(), this.oldCallback?.toString())) return true + return false + } +} + +export class MessagesQuery extends BaseQuery, MessageQueryOptions> { + override createQuery ( + params: MessageQueryParams, + callback: PagedQueryCallback, + options?: MessageQueryOptions + ): { unsubscribe: (isUpdate: boolean) => void } { + return getLiveQueries().queryMessages(params, callback, options) + } +} + +export class NotificationsQuery extends BaseQuery> { + override createQuery ( + params: NotificationQueryParams, + callback: PagedQueryCallback + ): { + unsubscribe: (isUpdate: boolean) => void + } { + return getLiveQueries().queryNotifications(params, callback) + } +} + +export class NotificationContextsQuery extends BaseQuery< +FindNotificationContextParams, +PagedQueryCallback, +NotificationContextQueryOptions +> { + override createQuery ( + params: FindNotificationContextParams, + callback: PagedQueryCallback, + options?: NotificationContextQueryOptions + ): { + unsubscribe: (isUpdate: boolean) => void + } { + return getLiveQueries().queryNotificationContexts(params, callback, options) + } +} + +export class LabelsQuery extends BaseQuery> { + override createQuery ( + params: FindLabelsParams, + callback: QueryCallback

( + cardId: CardID, + messageId: MessageID, + data: AttachmentData

[], + socialId: SocialID, + date?: Date + ) => Promise + removeAttachments: ( + cardId: CardID, + messageId: MessageID, + ids: AttachmentID[], + socialId: SocialID, + date?: Date + ) => Promise + setAttachments:

( + cardId: CardID, + messageId: MessageID, + data: AttachmentData

[], + socialId: SocialID, + date?: Date + ) => Promise +} diff --git a/foundations/communication/packages/rest-client/src/utils.ts b/foundations/communication/packages/rest-client/src/utils.ts new file mode 100644 index 0000000000..29314845dc --- /dev/null +++ b/foundations/communication/packages/rest-client/src/utils.ts @@ -0,0 +1,41 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { uncompress } from 'snappyjs' + +function isDateString (value: any): boolean { + if (typeof value !== 'string') return false + const dateStringRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/ + + return dateStringRegex.test(value) +} + +function reviver (key: string, value: any): Date { + if (isDateString(value)) return new Date(value) + return value +} + +export async function extractJson (response: Response): Promise { + const encoding = response.headers.get('content-encoding') + if (encoding === 'snappy') { + const buffer = await response.arrayBuffer() + const decompressed = uncompress(buffer) + const decoder = new TextDecoder() + const jsonString = decoder.decode(decompressed) + return JSON.parse(jsonString, reviver) as T + } + const jsonString = await response.text() + return JSON.parse(jsonString, reviver) as T +} diff --git a/foundations/communication/packages/rest-client/tsconfig.json b/foundations/communication/packages/rest-client/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/communication/packages/rest-client/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/communication/packages/sdk-types/.eslintrc.cjs b/foundations/communication/packages/sdk-types/.eslintrc.cjs new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/communication/packages/sdk-types/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/communication/packages/sdk-types/CHANGELOG.json b/foundations/communication/packages/sdk-types/CHANGELOG.json new file mode 100644 index 0000000000..396910c6d9 --- /dev/null +++ b/foundations/communication/packages/sdk-types/CHANGELOG.json @@ -0,0 +1,59 @@ +{ + "name": "@hcengineering/communication-sdk-types", + "entries": [ + { + "version": "0.7.12", + "tag": "@hcengineering/communication-sdk-types_v0.7.12", + "date": "Mon, 27 Oct 2025 16:28:25 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.11` to `0.7.12`" + } + ] + } + }, + { + "version": "0.7.9", + "tag": "@hcengineering/communication-sdk-types_v0.7.9", + "date": "Sat, 25 Oct 2025 22:20:35 GMT", + "comments": { + "patch": [ + { + "comment": "Fix peer create event" + }, + { + "comment": "Bump core" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.8` to `0.7.9`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/communication-sdk-types_v0.7.4", + "date": "Tue, 14 Oct 2025 10:12:38 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/communication/packages/sdk-types/CHANGELOG.md b/foundations/communication/packages/sdk-types/CHANGELOG.md new file mode 100644 index 0000000000..9d8560a863 --- /dev/null +++ b/foundations/communication/packages/sdk-types/CHANGELOG.md @@ -0,0 +1,26 @@ +# Change Log - @hcengineering/communication-sdk-types + +This log was last generated on Mon, 27 Oct 2025 16:28:25 GMT and should not be manually modified. + +## 0.7.12 +Mon, 27 Oct 2025 16:28:25 GMT + +### Patches + +- update deps + +## 0.7.9 +Sat, 25 Oct 2025 22:20:35 GMT + +### Patches + +- Fix peer create event +- Bump core + +## 0.7.4 +Tue, 14 Oct 2025 10:12:38 GMT + +### Patches + +- update deps + diff --git a/foundations/communication/packages/sdk-types/config/rig.json b/foundations/communication/packages/sdk-types/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/communication/packages/sdk-types/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/communication/packages/sdk-types/package.json b/foundations/communication/packages/sdk-types/package.json new file mode 100644 index 0000000000..80a6167a25 --- /dev/null +++ b/foundations/communication/packages/sdk-types/package.json @@ -0,0 +1,56 @@ +{ + "name": "@hcengineering/communication-sdk-types", + "version": "0.7.12", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "tsconfig.json" + ], + "scripts": { + "build": "compile", + "_phase:build": "compile transpile src", + "_phase:validate": "compile validate", + "lint": "eslint \"src/**/*.ts\"", + "lint:fix": "eslint --fix \"src/**/*.ts\"", + "format": "prettier --write src/**/*.ts && eslint --fix \"src/**/*.ts\" && echo 'Formatted'", + "clean": "rm -rf lib && rm -rf types rm -rf node_modules" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "esbuild": "^0.25.10", + "esbuild-plugin-copy": "^2.1.1", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "@types/jest": "^29.5.5", + "prettier": "^3.6.2", + "typescript": "^5.9.3" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/communication-types": "workspace:^0.7.12" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hcengineering/communication.git" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/communication/packages/sdk-types/src/client.ts b/foundations/communication/packages/sdk-types/src/client.ts new file mode 100644 index 0000000000..b44f7511d9 --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/client.ts @@ -0,0 +1,43 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { + CardID, + Collaborator, FindCollaboratorsParams, + FindLabelsParams, + FindMessagesMetaParams, + FindNotificationContextParams, + FindNotificationsParams, + Label, MessageMeta, + Notification, + NotificationContext, WithTotal +} from '@hcengineering/communication-types' + +import type { EventResult, Event } from './events/event' + +export interface FindClient { + onEvent: (event: Event) => void + onRequest: (event: Event, promise: Promise) => void + + findMessagesMeta: (params: FindMessagesMetaParams) => Promise + + findNotificationContexts: (params: FindNotificationContextParams, queryId?: number) => Promise + findNotifications: (params: FindNotificationsParams, queryId?: number) => Promise> + findLabels: (params: FindLabelsParams, queryId?: number) => Promise + findCollaborators: (params: FindCollaboratorsParams, queryId?: number) => Promise + + subscribeCard: (cardId: CardID, subscription: string | number) => Promise + unsubscribeCard: (cardId: CardID, subscription: string | number) => Promise +} diff --git a/foundations/communication/packages/sdk-types/src/db.ts b/foundations/communication/packages/sdk-types/src/db.ts new file mode 100644 index 0000000000..35648a8aaf --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/db.ts @@ -0,0 +1,147 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + CardID, + ContextID, + FindNotificationContextParams, + FindNotificationsParams, + MessageID, + NotificationContext, + SocialID, + Notification, + AccountUuid, + Collaborator, + FindCollaboratorsParams, + NotificationID, + Label, + FindLabelsParams, + LabelID, + CardType, + NotificationContent, + NotificationType, + WithTotal, + WorkspaceUuid, + PeerKind, + PeerExtra, + FindPeersParams, + Peer, + FindThreadMetaParams, + MessageMeta, + ThreadMeta, + FindMessagesMetaParams, + BlobID +} from '@hcengineering/communication-types' + +export interface DbAdapter { + // MessageMeta + createMessageMeta: ( + cardId: CardID, + id: MessageID, + creator: SocialID, + created: Date, + blob: BlobID + ) => Promise + removeMessageMeta: (cardId: CardID, messageId: MessageID) => Promise + findMessagesMeta: (params: FindMessagesMetaParams) => Promise + + // ThreadsIndex + attachThreadMeta: (cardId: CardID, messageId: MessageID, threadId: CardID, threadType: CardType, socialId: SocialID, date: Date) => Promise + removeThreadMeta: (query: ThreadMetaQuery) => Promise + updateThreadMeta: (query: ThreadMetaQuery, update: ThreadMetaUpdate) => Promise + findThreadMeta: (params: FindThreadMetaParams) => Promise + + // Peers + createPeer: ( + workspaceId: WorkspaceUuid, + cardId: CardID, + kind: PeerKind, + value: string, + extra: PeerExtra, + date: Date, + options?: { newValue?: boolean } + ) => Promise + removePeer: (workspaceId: WorkspaceUuid, + cardId: CardID, + kind: PeerKind, + value: string) => Promise + findPeers: (params: FindPeersParams) => Promise + + // Collaborators + addCollaborators: (cardId: CardID, cardType: CardType, collaborators: AccountUuid[], date: Date) => Promise + removeCollaborators: (query: CollaboratorQuery) => Promise + updateCollaborators: (query: CollaboratorQuery, update: CollaboratorUpdate) => Promise + getCollaboratorsCursor: (cardId: CardID, date: Date, size?: number) => AsyncIterable + findCollaborators: (params: FindCollaboratorsParams) => Promise + + // Notifications + createNotification: ( + contextId: ContextID, + messageId: MessageID, + blobId: BlobID, + type: NotificationType, + read: boolean, + content: NotificationContent, + creator: SocialID, + created: Date + ) => Promise + updateNotification: (query: NotificationQuery, updates: NotificationUpdate) => Promise + removeNotifications: (query: NotificationQuery) => Promise + findNotifications: (params: FindNotificationsParams) => Promise> + + // NotificationContext + createNotificationContext: ( + account: AccountUuid, + cardId: CardID, + lastUpdate: Date, + lastView: Date, + lastNotify: Date + ) => Promise + updateContext: (query: NotificationContextQuery, updates: NotificationContextUpdate) => Promise + removeContext: (query: NotificationContextQuery) => Promise + findNotificationContexts: (params: FindNotificationContextParams) => Promise + + // Labels + createLabel: (cardId: CardID, cardType: CardType, labelId: LabelID, account: AccountUuid, created: Date) => Promise + removeLabels: (query: LabelQuery) => Promise + updateLabels: (query: LabelQuery, update: LabelUpdate) => Promise + findLabels: (params: FindLabelsParams) => Promise + + // Other + getCardTitle: (cardId: CardID) => Promise + getCardSpaceMembers: (cardId: CardID) => Promise + getAccountsByPersonIds: (ids: string[]) => Promise + getNameByAccount: (id: AccountUuid) => Promise + + close: () => void +} + +export type ThreadMetaQuery = Partial> +export type ThreadMetaUpdate = Partial> + +export type LabelQuery = Partial> +export type LabelUpdate = Partial> + +export type NotificationContextQuery = Partial> +export type NotificationContextUpdate = Partial> + +export type NotificationQuery = Partial> & { + id?: NotificationID | NotificationID[] + untilDate?: Date +} +export type NotificationUpdate = Pick + +export type CollaboratorQuery = Pick & { account?: AccountUuid | AccountUuid[] } +export type CollaboratorUpdate = Partial diff --git a/foundations/communication/packages/sdk-types/src/domain.ts b/foundations/communication/packages/sdk-types/src/domain.ts new file mode 100644 index 0000000000..98aaf5cde6 --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/domain.ts @@ -0,0 +1,26 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +export enum Domain { + MessageIndex = 'communication.message_index', + ThreadIndex = 'communication.thread_index', + + Notification = 'communication.notification', + NotificationContext = 'communication.notification_context', + + Collaborator = 'communication.collaborator', + Label = 'communication.label', + Peer = 'communication.peer' +} + +export const Domains = Object.values(Domain) diff --git a/foundations/communication/packages/sdk-types/src/events/card.ts b/foundations/communication/packages/sdk-types/src/events/card.ts new file mode 100644 index 0000000000..e63e342e6c --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/events/card.ts @@ -0,0 +1,42 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { CardID, CardType, SocialID } from '@hcengineering/communication-types' + +import type { BaseEvent } from './common' + +export enum CardEventType { + UpdateCardType = 'updateCardType', + RemoveCard = 'removeCard' +} + +export type CardEvent = UpdateCardTypeEvent | RemoveCardEvent + +export interface UpdateCardTypeEvent extends BaseEvent { + type: CardEventType.UpdateCardType + cardId: CardID + cardType: CardType + + socialId: SocialID + date?: Date +} + +export interface RemoveCardEvent extends BaseEvent { + type: CardEventType.RemoveCard + cardId: CardID + + socialId: SocialID + date?: Date +} diff --git a/foundations/communication/packages/sdk-types/src/events/common.ts b/foundations/communication/packages/sdk-types/src/events/common.ts new file mode 100644 index 0000000000..ee911d0fac --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/events/common.ts @@ -0,0 +1,19 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export interface BaseEvent { + _id?: string + _eventExtra?: Record +} diff --git a/foundations/communication/packages/sdk-types/src/events/event.ts b/foundations/communication/packages/sdk-types/src/events/event.ts new file mode 100644 index 0000000000..506d4df622 --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/events/event.ts @@ -0,0 +1,29 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { LabelEvent, LabelEventType } from './label' +import type { MessageEventResult, MessageEventType, MessageEvent } from './message' +import type { NotificationEventResult, NotificationEvent, NotificationEventType } from './notification' +import type { CardEvent, CardEventType } from './card' +import { PeerEvent, PeerEventType } from './peer' + +export * from './message' +export * from './notification' +export * from './label' +export * from './card' +export * from './peer' + +export type EventType = MessageEventType | NotificationEventType | LabelEventType | CardEventType | PeerEventType +export type Event = MessageEvent | NotificationEvent | LabelEvent | CardEvent | PeerEvent +// eslint-disable-next-line @typescript-eslint/ban-types +export type EventResult = MessageEventResult | NotificationEventResult | {} diff --git a/foundations/communication/packages/sdk-types/src/events/label.ts b/foundations/communication/packages/sdk-types/src/events/label.ts new file mode 100644 index 0000000000..0c1735caac --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/events/label.ts @@ -0,0 +1,45 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { CardID, LabelID, CardType, AccountUuid } from '@hcengineering/communication-types' +import type { BaseEvent } from './common' + +export enum LabelEventType { + // Internal + CreateLabel = 'createLabel', + RemoveLabel = 'removeLabel' +} + +export type LabelEvent = CreateLabelEvent | RemoveLabelEvent + +// Internal +export interface CreateLabelEvent extends BaseEvent { + type: LabelEventType.CreateLabel + labelId: LabelID + cardId: CardID + cardType: CardType + account: AccountUuid + + date?: Date +} + +export interface RemoveLabelEvent extends BaseEvent { + type: LabelEventType.RemoveLabel + labelId: LabelID + cardId: CardID + account: AccountUuid + + date?: Date +} diff --git a/foundations/communication/packages/sdk-types/src/events/message.ts b/foundations/communication/packages/sdk-types/src/events/message.ts new file mode 100644 index 0000000000..bf491cf747 --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/events/message.ts @@ -0,0 +1,262 @@ +import type { + CardID, + MessageID, + Markdown, + SocialID, + BlobID, + MessageType, + CardType, + MessageExtra, + BlobParams, + AttachmentData, + AttachmentID, + AttachmentUpdateData, + Emoji, + PersonUuid +} from '@hcengineering/communication-types' + +import type { BaseEvent } from './common' + +export enum MessageEventType { + // Public events + CreateMessage = 'createMessage', + UpdatePatch = 'updatePatch', + RemovePatch = 'removePatch', + ReactionPatch = 'reactionPatch', + /** + * @deprecated Use AttachmentPatch instead + */ + BlobPatch = 'blobPatch', + AttachmentPatch = 'attachmentPatch', + ThreadPatch = 'threadPatch', + TranslateMessage = 'translateMessage' +} + +export type PatchEvent = + | UpdatePatchEvent + | RemovePatchEvent + | ReactionPatchEvent + | BlobPatchEvent + | AttachmentPatchEvent + | ThreadPatchEvent + +export type MessageEvent = CreateMessageEvent | PatchEvent | TranslateMessageEvent + +export interface CreateMessageOptions { + // Available for regular users (Not implemented yet) + skipLinkPreviews?: boolean + // Available only for system + noNotify?: boolean + // Dont add to collaborators mentioned users + ignoreMentions?: boolean +} +export interface UpdatePatchOptions { + // Available for regular users (Not implemented yet) + skipLinkPreviewsUpdate?: boolean + // Dont add to collaborators mentioned users + ignoreMentions?: boolean +} + +export interface CreateMessageEvent extends BaseEvent { + type: MessageEventType.CreateMessage + + cardId: CardID + cardType: CardType + + messageId?: MessageID + messageType: MessageType + + content: Markdown + language?: string + extra?: MessageExtra + + socialId: SocialID + date?: Date + + options?: CreateMessageOptions +} + +export interface TranslateMessageEvent extends BaseEvent { + type: MessageEventType.TranslateMessage + + cardId: CardID + messageId: MessageID + content: Markdown + language: string +} + +// Available for author and system +export interface UpdatePatchEvent extends BaseEvent { + type: MessageEventType.UpdatePatch + + cardId: CardID + messageId: MessageID + + content?: Markdown + extra?: MessageExtra + language?: string + + socialId: SocialID + date?: Date + + options?: UpdatePatchOptions +} + +// Available for author and system +export interface RemovePatchEvent extends BaseEvent { + type: MessageEventType.RemovePatch + + cardId: CardID + messageId: MessageID + + socialId: SocialID + date?: Date +} + +export interface AddReactionOperation { + opcode: 'add' + reaction: Emoji +} + +export interface RemoveReactionOperation { + opcode: 'remove' + reaction: Emoji +} + +// For any user +export interface ReactionPatchEvent extends BaseEvent { + type: MessageEventType.ReactionPatch + + cardId: CardID + messageId: MessageID + + operation: AddReactionOperation | RemoveReactionOperation + + socialId: SocialID + personUuid?: PersonUuid // Set by server + date?: Date +} + +export interface AttachBlobsOperation { + opcode: 'attach' + blobs: (BlobParams & { mimeType: string })[] +} + +export interface DetachBlobsOperation { + opcode: 'detach' + blobIds: BlobID[] +} + +export interface SetBlobsOperation { + opcode: 'set' + blobs: (BlobParams & { mimeType: string })[] +} + +export interface UpdateBlobsOperation { + opcode: 'update' + blobs: BlobUpdateData[] +} + +export type BlobUpdateData = { blobId: BlobID, mimeType?: string } & Partial + +/** + * @deprecated Use AttachmentPatch instead + */ +export interface BlobPatchEvent extends BaseEvent { + type: MessageEventType.BlobPatch + + cardId: CardID + messageId: MessageID + + operations: (AttachBlobsOperation | DetachBlobsOperation | SetBlobsOperation | UpdateBlobsOperation)[] + + socialId: SocialID + date?: Date +} + +export interface AddAttachmentsOperation { + opcode: 'add' + attachments: AttachmentData[] +} + +export interface RemoveAttachmentsOperation { + opcode: 'remove' + ids: AttachmentID[] +} + +export interface SetAttachmentsOperation { + opcode: 'set' + attachments: AttachmentData[] +} + +export interface UpdateAttachmentsOperation { + opcode: 'update' + attachments: AttachmentUpdateData[] +} + +// For system and message author +export interface AttachmentPatchEvent extends BaseEvent { + type: MessageEventType.AttachmentPatch + + cardId: CardID + messageId: MessageID + + operations: ( + | AddAttachmentsOperation + | RemoveAttachmentsOperation + | SetAttachmentsOperation + | UpdateAttachmentsOperation + )[] + + socialId: SocialID + date?: Date +} + +// For any user +export interface AttachThreadOperation { + opcode: 'attach' + threadId: CardID + threadType: CardType +} + +// For system +export interface UpdateThreadOperation { + opcode: 'update' + threadId: CardID + update: { + threadType: CardType + } +} + +// For system +export interface AddReplyOperation { + opcode: 'addReply' + threadId: CardID +} + +// For system +export interface RemoveReplyOperation { + opcode: 'removeReply' + threadId: CardID +} + +export interface ThreadPatchEvent extends BaseEvent { + type: MessageEventType.ThreadPatch + + cardId: CardID + messageId: MessageID + + operation: AttachThreadOperation | UpdateThreadOperation | AddReplyOperation | RemoveReplyOperation + + socialId: SocialID + personUuid?: PersonUuid // Set by server + date?: Date +} + +export interface CreateMessageResult { + messageId: MessageID + created: Date + blobId: BlobID +} + +export type MessageEventResult = CreateMessageResult diff --git a/foundations/communication/packages/sdk-types/src/events/notification.ts b/foundations/communication/packages/sdk-types/src/events/notification.ts new file mode 100644 index 0000000000..04d04ecee6 --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/events/notification.ts @@ -0,0 +1,156 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + CardID, + ContextID, + MessageID, + AccountUuid, + CardType, + NotificationType, + NotificationContent, + NotificationID, + BlobID, + SocialID +} from '@hcengineering/communication-types' + +import type { BaseEvent } from './common' + +export enum NotificationEventType { + AddCollaborators = 'addCollaborators', + RemoveCollaborators = 'removeCollaborators', + + CreateNotification = 'createNotification', + RemoveNotifications = 'removeNotifications', + UpdateNotification = 'updateNotification', + + CreateNotificationContext = 'createNotificationContext', + RemoveNotificationContext = 'removeNotificationContext', + UpdateNotificationContext = 'updateNotificationContext' +} + +export type NotificationEvent = + | AddCollaboratorsEvent + | CreateNotificationContextEvent + | CreateNotificationEvent + | UpdateNotificationEvent + | RemoveCollaboratorsEvent + | RemoveNotificationContextEvent + | RemoveNotificationsEvent + | UpdateNotificationContextEvent + +export interface CreateNotificationEvent extends BaseEvent { + type: NotificationEventType.CreateNotification + + notificationId?: NotificationID + notificationType: NotificationType + read: boolean + content: NotificationContent + cardId: CardID + contextId: ContextID + messageId: MessageID + creator: SocialID + blobId: BlobID + account: AccountUuid + + date?: Date +} + +export interface UpdateNotificationEvent extends BaseEvent { + type: NotificationEventType.UpdateNotification + contextId: ContextID + account: AccountUuid + query: { + type?: NotificationType + id?: NotificationID + untilDate?: Date + } + updates: { + read: boolean + } + + date?: Date + updated?: number +} + +export interface RemoveNotificationsEvent extends BaseEvent { + type: NotificationEventType.RemoveNotifications + contextId: ContextID + account: AccountUuid + ids: NotificationID[] + + date?: Date +} + +export interface CreateNotificationContextEvent extends BaseEvent { + type: NotificationEventType.CreateNotificationContext + contextId?: ContextID + cardId: CardID + account: AccountUuid + + lastView: Date + lastUpdate: Date + lastNotify: Date + + date?: Date +} + +export interface RemoveNotificationContextEvent extends BaseEvent { + type: NotificationEventType.RemoveNotificationContext + contextId: ContextID + account: AccountUuid + + date?: Date +} + +export interface UpdateNotificationContextEvent extends BaseEvent { + type: NotificationEventType.UpdateNotificationContext + contextId: ContextID + account: AccountUuid + updates: { + lastView?: Date + lastUpdate?: Date + lastNotify?: Date + } + + date?: Date +} + +export interface AddCollaboratorsEvent extends BaseEvent { + type: NotificationEventType.AddCollaborators + cardId: CardID + cardType: CardType + collaborators: AccountUuid[] + + socialId: SocialID + date?: Date +} + +export interface RemoveCollaboratorsEvent extends BaseEvent { + type: NotificationEventType.RemoveCollaborators + cardId: CardID + cardType: CardType + collaborators: AccountUuid[] + + socialId: SocialID + date?: Date +} + +// eslint-disable-next-line @typescript-eslint/ban-types +export type NotificationEventResult = CreateNotificationContextResult | {} + +export interface CreateNotificationContextResult { + id: ContextID +} diff --git a/foundations/communication/packages/sdk-types/src/events/peer.ts b/foundations/communication/packages/sdk-types/src/events/peer.ts new file mode 100644 index 0000000000..0664469f04 --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/events/peer.ts @@ -0,0 +1,47 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { BaseEvent } from './common' +import { CardID, PeerKind, PeerExtra, WorkspaceUuid } from '@hcengineering/communication-types' + +// Peer events only for system +export enum PeerEventType { + CreatePeer = 'createPeer', + RemovePeer = 'removePeer' +} + +export type PeerEvent = CreatePeerEvent | RemovePeerEvent + +export interface CreatePeerEvent extends BaseEvent { + type: PeerEventType.CreatePeer + workspaceId: WorkspaceUuid + cardId: CardID + kind: PeerKind + value: string + extra?: PeerExtra + options?: { + newValue?: boolean + } + date?: Date +} + +export interface RemovePeerEvent extends BaseEvent { + type: PeerEventType.RemovePeer + workspaceId: WorkspaceUuid + cardId: CardID + kind: PeerKind + value: string + date?: Date +} diff --git a/foundations/communication/packages/sdk-types/src/index.ts b/foundations/communication/packages/sdk-types/src/index.ts new file mode 100644 index 0000000000..901d8da521 --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/index.ts @@ -0,0 +1,21 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export type * from './client' +export * from './db' +export type * from './query' +export * from './events/event' +export type * from './serverApi' +export * from './domain' diff --git a/foundations/communication/packages/sdk-types/src/query.ts b/foundations/communication/packages/sdk-types/src/query.ts new file mode 100644 index 0000000000..2275ebd1ee --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/query.ts @@ -0,0 +1,19 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Window } from '@hcengineering/communication-types' + +export type PagedQueryCallback = (window: Window) => void +export type QueryCallback = (result: T[]) => void diff --git a/foundations/communication/packages/sdk-types/src/serverApi.ts b/foundations/communication/packages/sdk-types/src/serverApi.ts new file mode 100644 index 0000000000..b38d582b0d --- /dev/null +++ b/foundations/communication/packages/sdk-types/src/serverApi.ts @@ -0,0 +1,70 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { + FindNotificationContextParams, + FindNotificationsParams, + NotificationContext, + Notification, + FindLabelsParams, + Label, + FindCollaboratorsParams, + Collaborator, + FindPeersParams, Peer, CardID, FindMessagesMetaParams, MessageMeta, MessagesGroup, FindMessagesGroupParams +} from '@hcengineering/communication-types' +import type { Account, MeasureContext } from '@hcengineering/core' + +import type { EventResult, Event } from './events/event' + +export type ContextData = { + asyncRequests?: ((ctx: MeasureContext) => Promise)[] +} & Record + +export interface SessionData { + sessionId?: string + account: Account + derived?: boolean + isAsyncContext?: boolean + contextData?: ContextData + asyncData: Event[] +} + +export interface ServerApi { + findMessagesGroups: (session: SessionData, params: FindMessagesGroupParams) => Promise + findMessagesMeta: (session: SessionData, params: FindMessagesMetaParams) => Promise + + findNotificationContexts: ( + session: SessionData, + params: FindNotificationContextParams, + subscription?: number | string + ) => Promise + findNotifications: ( + session: SessionData, + params: FindNotificationsParams, + subscription?: number | string + ) => Promise + + findLabels: (session: SessionData, params: FindLabelsParams) => Promise + findCollaborators: (session: SessionData, params: FindCollaboratorsParams) => Promise + findPeers: (session: SessionData, params: FindPeersParams) => Promise + + event: (session: SessionData, event: Event) => Promise + + subscribeCard: (session: SessionData, cardId: CardID, subscription: number | string) => void + unsubscribeCard: (session: SessionData, cardId: CardID, subscription: number | string) => void + + closeSession: (sessionId: string) => Promise + close: () => Promise +} diff --git a/foundations/communication/packages/sdk-types/tsconfig.json b/foundations/communication/packages/sdk-types/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/communication/packages/sdk-types/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/communication/packages/server/.eslintrc.cjs b/foundations/communication/packages/server/.eslintrc.cjs new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/communication/packages/server/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/communication/packages/server/CHANGELOG.json b/foundations/communication/packages/server/CHANGELOG.json new file mode 100644 index 0000000000..8bfd469579 --- /dev/null +++ b/foundations/communication/packages/server/CHANGELOG.json @@ -0,0 +1,92 @@ +{ + "name": "@hcengineering/communication-server", + "entries": [ + { + "version": "0.7.12", + "tag": "@hcengineering/communication-server_v0.7.12", + "date": "Mon, 27 Oct 2025 16:28:25 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.11` to `0.7.12`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.11` to `0.7.12`" + } + ] + } + }, + { + "version": "0.7.9", + "tag": "@hcengineering/communication-server_v0.7.9", + "date": "Sat, 25 Oct 2025 22:20:35 GMT", + "comments": { + "patch": [ + { + "comment": "Fix peer create" + }, + { + "comment": "Remove reaction notification on reaction unset" + }, + { + "comment": "Improve validation of find methods params" + }, + { + "comment": "Add tests" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-cockroach\" from `^0.7.8` to `0.7.9`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.8` to `0.7.9`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.8` to `0.7.9`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/communication-server_v0.7.4", + "date": "Tue, 14 Oct 2025 10:12:38 GMT", + "comments": { + "patch": [ + { + "comment": "fix language store" + }, + { + "comment": "Do not update message.modified date if content is not changed " + }, + { + "comment": "update hulylake client" + }, + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-cockroach\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-shared\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/communication/packages/server/CHANGELOG.md b/foundations/communication/packages/server/CHANGELOG.md new file mode 100644 index 0000000000..629d6be9cc --- /dev/null +++ b/foundations/communication/packages/server/CHANGELOG.md @@ -0,0 +1,31 @@ +# Change Log - @hcengineering/communication-server + +This log was last generated on Mon, 27 Oct 2025 16:28:25 GMT and should not be manually modified. + +## 0.7.12 +Mon, 27 Oct 2025 16:28:25 GMT + +### Patches + +- update deps + +## 0.7.9 +Sat, 25 Oct 2025 22:20:35 GMT + +### Patches + +- Fix peer create +- Remove reaction notification on reaction unset +- Improve validation of find methods params +- Add tests + +## 0.7.4 +Tue, 14 Oct 2025 10:12:38 GMT + +### Patches + +- fix language store +- Do not update message.modified date if content is not changed +- update hulylake client +- update deps + diff --git a/foundations/communication/packages/server/config/rig.json b/foundations/communication/packages/server/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/communication/packages/server/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/communication/packages/server/jest.config.js b/foundations/communication/packages/server/jest.config.js new file mode 100644 index 0000000000..c137fcf3e0 --- /dev/null +++ b/foundations/communication/packages/server/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + collectCoverage: true, + coverageReporters: ['text-summary', 'html', 'lcov'], + coverageDirectory: 'coverage', + moduleNameMapper: { + '^franc-min$': '/src/__mocks__/franc-min.ts' + }, + transformIgnorePatterns: [ + 'node_modules/(?!(franc-min|trigram-utils)/)' + ], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] +} diff --git a/foundations/communication/packages/server/package.json b/foundations/communication/packages/server/package.json new file mode 100644 index 0000000000..62fce803e7 --- /dev/null +++ b/foundations/communication/packages/server/package.json @@ -0,0 +1,71 @@ +{ + "name": "@hcengineering/communication-server", + "version": "0.7.12", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "tsconfig.json" + ], + "scripts": { + "build": "compile", + "_phase:build": "compile transpile src", + "_phase:validate": "compile validate", + "lint": "eslint \"src/**/*.ts\"", + "lint:fix": "eslint --fix \"src/**/*.ts\"", + "format": "prettier --write src/**/*.ts && eslint --fix \"src/**/*.ts\" && echo 'Formatted'", + "clean": "rm -rf lib && rm -rf types rm -rf node_modules", + "test": "jest --passWithNoTests --silent --forceExit", + "_phase:test": "jest --passWithNoTests --silent --forceExit" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@types/jest": "^29.5.5", + "@types/node": "^22.18.1", + "@types/uuid": "^8.3.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "esbuild": "^0.25.10", + "esbuild-plugin-copy": "^2.1.1", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "prettier": "^3.6.2", + "ts-jest": "^29.1.1", + "typescript": "^5.9.3" + }, + "dependencies": { + "@hcengineering/account-client": "workspace:^0.7.19", + "@hcengineering/communication-cockroach": "workspace:^0.7.11", + "@hcengineering/communication-sdk-types": "workspace:^0.7.12", + "@hcengineering/communication-shared": "workspace:^0.7.11", + "@hcengineering/communication-types": "workspace:^0.7.12", + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/server-token": "workspace:^0.7.17", + "@hcengineering/text-core": "workspace:^0.7.18", + "@hcengineering/text-markdown": "workspace:^0.7.20", + "@hcengineering/hulylake-client": "workspace:^0.7.17", + "uuid": "^8.3.2", + "zod": "^3.22.4" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hcengineering/communication.git" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/communication/packages/server/src/__mocks__/notification/notification.ts b/foundations/communication/packages/server/src/__mocks__/notification/notification.ts new file mode 100644 index 0000000000..7101e0dfbc --- /dev/null +++ b/foundations/communication/packages/server/src/__mocks__/notification/notification.ts @@ -0,0 +1,2 @@ +// Mock for notification/notification +export const notify = jest.fn().mockResolvedValue([]) diff --git a/foundations/communication/packages/server/src/__mocks__/triggers/all.ts b/foundations/communication/packages/server/src/__mocks__/triggers/all.ts new file mode 100644 index 0000000000..faf1ab60ac --- /dev/null +++ b/foundations/communication/packages/server/src/__mocks__/triggers/all.ts @@ -0,0 +1,2 @@ +// Mock for triggers/all +export default [] diff --git a/foundations/communication/packages/server/src/__tests__/blob.test.ts b/foundations/communication/packages/server/src/__tests__/blob.test.ts new file mode 100644 index 0000000000..892940b66e --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/blob.test.ts @@ -0,0 +1,947 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MeasureContext, SortingOrder, WorkspaceUuid, PersonUuid } from '@hcengineering/core' +import { HulylakeWorkspaceClient, getWorkspaceClient } from '@hcengineering/hulylake-client' +import { + CardID, + BlobID, + MessageID, + MessagesGroup, + Message, + Markdown, + MessageExtra, + Attachment, + AttachmentID, + Thread, + CardType, + AttachmentUpdateData, + MessageType, + SocialID +} from '@hcengineering/communication-types' +import { Blob } from '../blob' +import { Metadata } from '../types' + +// Mock dependencies +jest.mock('@hcengineering/hulylake-client') +jest.mock('@hcengineering/server-token', () => ({ + generateToken: jest.fn(() => 'mock-token') +})) +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'mock-uuid') +})) + +describe('Blob', () => { + let blob: Blob + let mockClient: jest.Mocked + let mockCtx: jest.Mocked + let mockMetadata: Metadata + + const workspace = 'test-workspace' as WorkspaceUuid + const cardId = 'test-card-id' as CardID + const blobId = 'test-blob-id' as BlobID + const messageId = 'test-message-id' as MessageID + + beforeEach(() => { + // Setup mock context + mockCtx = { + warn: jest.fn(), + error: jest.fn(), + info: jest.fn() + } as any + + // Setup mock metadata + mockMetadata = { + accountsUrl: 'http://accounts.test', + hulylakeUrl: 'http://hulylake.test', + secret: 'test-secret', + messagesPerBlob: 100 + } + + // Setup mock client + mockClient = { + getJson: jest.fn(), + putJson: jest.fn(), + patchJson: jest.fn() + } as any + + // Mock getWorkspaceClient to return our mock client + ;(getWorkspaceClient as jest.Mock).mockReturnValue(mockClient) + + blob = new Blob(mockCtx, workspace, mockMetadata) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('findMessagesGroups', () => { + beforeEach(() => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-02T00:00:00.000Z', + count: 10 + }, + 'blob-2': { + cardId, + blobId: 'blob-2' as BlobID, + fromDate: '2025-01-03T00:00:00.000Z', + toDate: '2025-01-04T00:00:00.000Z', + count: 20 + }, + 'blob-3': { + cardId, + blobId: 'blob-3' as BlobID, + fromDate: '2025-01-05T00:00:00.000Z', + toDate: '2025-01-06T00:00:00.000Z', + count: 30 + } + } + } as any) + }) + + it('should return all groups when no filters are provided', async () => { + const result = await blob.findMessagesGroups({ cardId }) + + expect(result).toHaveLength(3) + expect(result[0].blobId).toBe('blob-1') + expect(result[1].blobId).toBe('blob-2') + expect(result[2].blobId).toBe('blob-3') + }) + + it('should sort groups in ascending order', async () => { + const result = await blob.findMessagesGroups({ + cardId, + order: SortingOrder.Ascending + }) + + expect(result[0].fromDate.getTime()).toBeLessThan(result[1].fromDate.getTime()) + expect(result[1].fromDate.getTime()).toBeLessThan(result[2].fromDate.getTime()) + }) + + it('should sort groups in descending order', async () => { + const result = await blob.findMessagesGroups({ + cardId, + order: SortingOrder.Descending + }) + + expect(result[0].fromDate.getTime()).toBeGreaterThan(result[1].fromDate.getTime()) + expect(result[1].fromDate.getTime()).toBeGreaterThan(result[2].fromDate.getTime()) + }) + + it('should filter groups by blobId', async () => { + const result = await blob.findMessagesGroups({ + cardId, + blobId: 'blob-2' as BlobID + }) + + expect(result).toHaveLength(1) + expect(result[0].blobId).toBe('blob-2') + }) + + it('should filter groups by fromDate', async () => { + const result = await blob.findMessagesGroups({ + cardId, + fromDate: new Date('2025-01-03') + }) + + expect(result).toHaveLength(1) + expect(result[0].blobId).toBe('blob-2') + }) + + it('should filter groups by toDate', async () => { + const result = await blob.findMessagesGroups({ + cardId, + toDate: new Date('2025-01-04') + }) + + expect(result).toHaveLength(1) + expect(result[0].blobId).toBe('blob-2') + }) + + it('should limit the number of returned groups', async () => { + const result = await blob.findMessagesGroups({ + cardId, + limit: 2 + }) + + expect(result).toHaveLength(2) + }) + + it('should create groups blob when not found (404)', async () => { + mockClient.getJson.mockResolvedValue({ + status: 404, + body: null + } as any) + + const result = await blob.findMessagesGroups({ cardId }) + + expect(result).toHaveLength(0) + expect(mockClient.putJson).toHaveBeenCalledWith(`${cardId}/messages/groups`, {}, undefined, expect.any(Object)) + }) + }) + + describe('getMessageGroupByDate', () => { + beforeEach(() => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-10T00:00:00.000Z', + count: 10 + } + } + } as any) + }) + + it('should return matching group when date is within range', async () => { + const result = await blob.getMessageGroupByDate(cardId, new Date('2025-01-05'), false) + + expect(result).toBeDefined() + expect(result?.blobId).toBe('blob-1') + }) + + it('should return last group if date is after and group is not full', async () => { + const result = await blob.getMessageGroupByDate(cardId, new Date('2025-01-15'), false) + + expect(result).toBeDefined() + expect(result?.blobId).toBe('blob-1') + }) + + it('should return undefined when no match and create is false', async () => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-10T00:00:00.000Z', + count: 100 // Full group + } + } + } as any) + + const result = await blob.getMessageGroupByDate(cardId, new Date('2025-01-15'), false) + + expect(result).toBeUndefined() + }) + + it('should create new group when no match and create is true', async () => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: {} + } as any) + + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + mockClient.putJson.mockResolvedValue({ status: 200 } as any) + + const result = await blob.getMessageGroupByDate(cardId, new Date('2025-01-15'), true) + + expect(result).toBeDefined() + expect(mockClient.patchJson).toHaveBeenCalled() + expect(mockClient.putJson).toHaveBeenCalled() + }) + }) + + describe('insertMessage', () => { + const mockGroup: MessagesGroup = { + cardId, + blobId, + fromDate: new Date('2025-01-01'), + toDate: new Date('2025-01-10'), + count: 5 + } + + const mockMessage: Message = { + id: messageId, + created: new Date('2025-01-05'), + creator: 'user-1' as SocialID, + content: 'Test message' as Markdown, + cardId, + type: MessageType.Text, + reactions: {}, + attachments: [], + threads: [] + } + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + [blobId]: { + cardId, + blobId, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-10T00:00:00.000Z', + count: 5 + } + } + } as any) + }) + + it('should insert message with correct patches', async () => { + await blob.insertMessage(cardId, mockGroup, mockMessage) + + expect(mockClient.patchJson).toHaveBeenCalledWith( + `${cardId}/messages/${blobId}`, + expect.arrayContaining([ + expect.objectContaining({ + hop: 'add', + path: `/messages/${messageId}` + }) + ]), + undefined, + expect.any(Object) + ) + }) + + it('should update toDate when message is newer', async () => { + const newerMessage = { + ...mockMessage, + created: new Date('2025-01-15') + } + + await blob.insertMessage(cardId, mockGroup, newerMessage) + + const patches = mockClient.patchJson.mock.calls[0][1] + const toDatePatch = patches.find((p) => p.path === '/toDate') + expect(toDatePatch).toBeDefined() + }) + + it('should update fromDate when message is older', async () => { + const olderMessage = { + ...mockMessage, + created: new Date('2024-12-31') + } + + await blob.insertMessage(cardId, mockGroup, olderMessage) + + const patches = mockClient.patchJson.mock.calls[0][1] + const fromDatePatch = patches.find((p) => p.path === '/fromDate') + expect(fromDatePatch).toBeDefined() + }) + }) + + describe('updateMessage', () => { + const date = new Date('2025-01-05') + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should update message content', async () => { + await blob.updateMessage(cardId, blobId, messageId, { content: 'Updated content' as Markdown }, date) + + const patches = mockClient.patchJson.mock.calls[0][1] + expect(patches.some((p) => p.path === `/messages/${messageId}/content`)).toBe(true) + expect(patches.some((p) => p.path === `/messages/${messageId}/modified`)).toBe(true) + }) + + it('should update message extra', async () => { + const extra: MessageExtra = { key: 'value' } + await blob.updateMessage(cardId, blobId, messageId, { extra }, date) + + const patches = mockClient.patchJson.mock.calls[0][1] + expect(patches.some((p) => p.path === `/messages/${messageId}/extra`)).toBe(true) + }) + + it('should update message language without modifying modified field', async () => { + await blob.updateMessage(cardId, blobId, messageId, { language: 'en' }, date) + + const patches = mockClient.patchJson.mock.calls[0][1] + expect(patches.some((p) => p.path === `/messages/${messageId}/language`)).toBe(true) + expect(patches.some((p) => p.path === `/messages/${messageId}/modified`)).toBe(false) + }) + + it('should not call patchJson when no updates provided', async () => { + await blob.updateMessage(cardId, blobId, messageId, {}, date) + + expect(mockClient.patchJson).not.toHaveBeenCalled() + }) + }) + + describe('removeMessage', () => { + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + [blobId]: { + cardId, + blobId, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-10T00:00:00.000Z', + count: 5 + } + } + } as any) + }) + + it('should remove message with correct patch', async () => { + await blob.removeMessage(cardId, blobId, messageId) + + expect(mockClient.patchJson).toHaveBeenCalledWith( + `${cardId}/messages/${blobId}`, + expect.arrayContaining([ + expect.objectContaining({ + hop: 'remove', + path: `/messages/${messageId}`, + safe: true + }) + ]), + undefined, + expect.any(Object) + ) + }) + }) + + describe('addReaction', () => { + const person = 'user-1' as PersonUuid + const emoji = '👍' + const date = new Date('2025-01-05') + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should add reaction with correct patches', async () => { + await blob.addReaction(cardId, blobId, messageId, emoji, person, date) + + expect(mockClient.patchJson).toHaveBeenCalledWith( + `${cardId}/messages/${blobId}`, + expect.arrayContaining([ + expect.objectContaining({ + hop: 'add', + path: `/messages/${messageId}/reactions/${emoji}` + }), + expect.objectContaining({ + hop: 'add', + path: `/messages/${messageId}/reactions/${emoji}/${person}` + }) + ]), + undefined, + expect.any(Object) + ) + }) + }) + + describe('removeReaction', () => { + const person = 'user-1' as PersonUuid + const emoji = '👍' + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should remove reaction with correct patch', async () => { + await blob.removeReaction(cardId, blobId, messageId, emoji, person) + + expect(mockClient.patchJson).toHaveBeenCalledWith( + `${cardId}/messages/${blobId}`, + expect.arrayContaining([ + expect.objectContaining({ + hop: 'remove', + path: `/messages/${messageId}/reactions/${emoji}/${person}`, + safe: true + }) + ]), + undefined, + expect.any(Object) + ) + }) + }) + + describe('addAttachments', () => { + const attachments: Attachment[] = [ + { + id: 'att-1' as AttachmentID, + mimeType: 'text/plain', + params: { blobId: 'blob-1' as BlobID, fileName: 'file1.txt', size: 1024 }, + creator: 'user-1' as SocialID, + created: new Date('2025-01-05') + }, + { + id: 'att-2' as AttachmentID, + mimeType: 'text/plain', + params: { blobId: 'blob-2' as BlobID, fileName: 'file2.txt', size: 2048 }, + creator: 'user-1' as SocialID, + created: new Date('2025-01-05') + } + ] + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should add multiple attachments', async () => { + await blob.addAttachments(cardId, blobId, messageId, attachments) + + const patches = mockClient.patchJson.mock.calls[0][1] + expect(patches).toHaveLength(2) + expect(patches[0].path).toBe(`/messages/${messageId}/attachments/att-1`) + expect(patches[1].path).toBe(`/messages/${messageId}/attachments/att-2`) + }) + }) + + describe('removeAttachments', () => { + const attachmentIds: AttachmentID[] = ['att-1' as AttachmentID, 'att-2' as AttachmentID] + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should remove multiple attachments', async () => { + await blob.removeAttachments(cardId, blobId, messageId, attachmentIds) + + const patches = mockClient.patchJson.mock.calls[0][1] + expect(patches).toHaveLength(2) + expect(patches[0].path).toBe(`/messages/${messageId}/attachments/att-1`) + expect(patches[1].path).toBe(`/messages/${messageId}/attachments/att-2`) + }) + }) + + describe('setAttachments', () => { + const attachments: Attachment[] = [ + { + id: 'att-1' as AttachmentID, + mimeType: 'text/plain', + params: { blobId: 'blob-1' as BlobID, fileName: 'file1.txt', size: 1024 }, + creator: 'user-1' as SocialID, + created: new Date('2025-01-05') + } + ] + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should replace all attachments', async () => { + await blob.setAttachments(cardId, blobId, messageId, attachments) + + const patches = mockClient.patchJson.mock.calls[0][1] + expect(patches[0].path).toBe(`/messages/${messageId}/attachments`) + }) + }) + + describe('updateAttachments', () => { + const updates: AttachmentUpdateData[] = [ + { + id: 'att-1' as AttachmentID, + params: { status: 'processed' } + } + ] + const date = new Date('2025-01-05') + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should update attachment parameters', async () => { + await blob.updateAttachments(cardId, blobId, messageId, updates, date) + + const patches = mockClient.patchJson.mock.calls[0][1] + expect(patches.some((p) => p.path.includes('/params/status'))).toBe(true) + expect(patches.some((p) => p.path.includes('/modified'))).toBe(true) + }) + + it('should skip updates with empty params', async () => { + const emptyUpdates: AttachmentUpdateData[] = [ + { + id: 'att-1' as AttachmentID, + params: {} + } + ] + + await blob.updateAttachments(cardId, blobId, messageId, emptyUpdates, date) + + expect(mockClient.patchJson).toHaveBeenCalledWith(expect.any(String), [], undefined, expect.any(Object)) + }) + }) + + describe('attachThread', () => { + const thread: Thread = { + cardId, + messageId, + threadId: 'thread-1' as CardID, + threadType: 'discussion' as CardType, + repliesCount: 0, + lastReplyDate: undefined, + repliedPersons: {} + } + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should attach thread to message', async () => { + await blob.attachThread(cardId, blobId, messageId, thread) + + expect(mockClient.patchJson).toHaveBeenCalledWith( + `${cardId}/messages/${blobId}`, + expect.arrayContaining([ + expect.objectContaining({ + op: 'add', + path: `/messages/${messageId}/threads/${thread.threadId}` + }) + ]), + undefined, + expect.any(Object) + ) + }) + }) + + describe('updateThread', () => { + const threadId = 'thread-1' as CardID + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should update thread type', async () => { + await blob.updateThread(cardId, blobId, messageId, threadId, { + threadType: 'task' as CardType + }) + + expect(mockClient.patchJson).toHaveBeenCalledWith( + `${cardId}/messages/${blobId}`, + expect.arrayContaining([ + expect.objectContaining({ + op: 'add', + path: `/messages/${messageId}/threads/${threadId}/threadType`, + value: 'task' + }) + ]), + undefined, + expect.any(Object) + ) + }) + }) + + describe('addThreadReply', () => { + const threadId = 'thread-1' as CardID + const person = 'user-1' as PersonUuid + const date = new Date('2025-01-05') + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should increment replies count and update last reply', async () => { + await blob.addThreadReply(cardId, blobId, messageId, threadId, person, date) + + const patches = mockClient.patchJson.mock.calls[0][1] + expect(patches.some((p) => p.path.includes('/repliesCount'))).toBe(true) + expect(patches.some((p) => p.path.includes('/lastReply'))).toBe(true) + expect(patches.some((p) => p.path.includes('/repliedPersons'))).toBe(true) + }) + }) + + describe('removeThreadReply', () => { + const threadId = 'thread-1' as CardID + const person = 'user-1' as PersonUuid + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should decrement replies count', async () => { + await blob.removeThreadReply(cardId, blobId, messageId, threadId, person) + + const patches = mockClient.patchJson.mock.calls[0][1] + const repliesCountPatch = patches.find((p) => p.path.includes('/repliesCount')) + expect(repliesCountPatch).toBeDefined() + expect((repliesCountPatch as any).value).toBe(-1) + }) + }) + + describe('removeThread', () => { + const threadId = 'thread-1' as CardID + + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should remove thread from message', async () => { + await blob.removeThread(cardId, blobId, messageId, threadId) + + expect(mockClient.patchJson).toHaveBeenCalledWith( + `${cardId}/messages/${blobId}`, + expect.arrayContaining([ + expect.objectContaining({ + hop: 'remove', + path: `/messages/${messageId}/threads/${threadId}`, + safe: true + }) + ]), + undefined, + expect.any(Object) + ) + }) + }) + + describe('Message group selection logic', () => { + beforeEach(() => { + mockClient.patchJson.mockResolvedValue({ status: 200 } as any) + mockClient.putJson.mockResolvedValue({ status: 200 } as any) + }) + + it('should insert message into group that matches date range', async () => { + // Setup: three groups with different date ranges + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-05T00:00:00.000Z', + count: 10 + }, + 'blob-2': { + cardId, + blobId: 'blob-2' as BlobID, + fromDate: '2025-01-06T00:00:00.000Z', + toDate: '2025-01-10T00:00:00.000Z', + count: 20 + }, + 'blob-3': { + cardId, + blobId: 'blob-3' as BlobID, + fromDate: '2025-01-11T00:00:00.000Z', + toDate: '2025-01-15T00:00:00.000Z', + count: 30 + } + } + } as any) + + // Message with date in the range of the second group + const messageDate = new Date('2025-01-08T12:00:00.000Z') + const group = await blob.getMessageGroupByDate(cardId, messageDate, false) + + expect(group).toBeDefined() + expect(group?.blobId).toBe('blob-2') + expect(group?.fromDate.getTime()).toBeLessThanOrEqual(messageDate.getTime()) + expect(group?.toDate.getTime()).toBeGreaterThanOrEqual(messageDate.getTime()) + }) + + it('should insert message into last group if date is after all groups and group is not full', async () => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-05T00:00:00.000Z', + count: 50 // Not full (< 100) + } + } + } as any) + + // Message with date after the last group + const messageDate = new Date('2025-01-10T12:00:00.000Z') + const group = await blob.getMessageGroupByDate(cardId, messageDate, false) + + expect(group).toBeDefined() + expect(group?.blobId).toBe('blob-1') + expect(group?.count).toBeLessThan(mockMetadata.messagesPerBlob) + }) + + it('should insert message into first group if date is before all groups and group is not full', async () => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-10T00:00:00.000Z', + toDate: '2025-01-15T00:00:00.000Z', + count: 50 // Not full + } + } + } as any) + + // Message with date before the first group + const messageDate = new Date('2025-01-05T12:00:00.000Z') + const group = await blob.getMessageGroupByDate(cardId, messageDate, false) + + expect(group).toBeDefined() + expect(group?.blobId).toBe('blob-1') + expect(group?.count).toBeLessThan(mockMetadata.messagesPerBlob) + }) + + it('should NOT use last group if it is full', async () => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-05T00:00:00.000Z', + count: 100 // Full group + } + } + } as any) + + // Message with date after the last group + const messageDate = new Date('2025-01-10T12:00:00.000Z') + const group = await blob.getMessageGroupByDate(cardId, messageDate, false) + + // Should not find a group since the only group is full + expect(group).toBeUndefined() + }) + + it('should NOT use first group if it is full', async () => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-10T00:00:00.000Z', + toDate: '2025-01-15T00:00:00.000Z', + count: 100 // Full group + } + } + } as any) + + // Message with date before the first group + const messageDate = new Date('2025-01-05T12:00:00.000Z') + const group = await blob.getMessageGroupByDate(cardId, messageDate, false) + + // Should not find a group since the only group is full + expect(group).toBeUndefined() + }) + + it('should create new group when no suitable group exists and create=true', async () => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-05T00:00:00.000Z', + count: 100 // Full group + } + } + } as any) + + const messageDate = new Date('2025-01-10T12:00:00.000Z') + const group = await blob.getMessageGroupByDate(cardId, messageDate, true) + + expect(group).toBeDefined() + expect(mockClient.patchJson).toHaveBeenCalled() + expect(mockClient.putJson).toHaveBeenCalled() + }) + + it('should select correct group among multiple groups', async () => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-03T00:00:00.000Z', + count: 100 // Full + }, + 'blob-2': { + cardId, + blobId: 'blob-2' as BlobID, + fromDate: '2025-01-04T00:00:00.000Z', + toDate: '2025-01-06T00:00:00.000Z', + count: 50 // Not full + }, + 'blob-3': { + cardId, + blobId: 'blob-3' as BlobID, + fromDate: '2025-01-07T00:00:00.000Z', + toDate: '2025-01-09T00:00:00.000Z', + count: 100 // Full + } + } + } as any) + + // Message should go into blob-2 since its date is in range + const messageDate = new Date('2025-01-05T12:00:00.000Z') + const group = await blob.getMessageGroupByDate(cardId, messageDate, false) + + expect(group).toBeDefined() + expect(group?.blobId).toBe('blob-2') + }) + + it('should use last non-full group for date after all groups', async () => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-03T00:00:00.000Z', + count: 100 // Full + }, + 'blob-2': { + cardId, + blobId: 'blob-2' as BlobID, + fromDate: '2025-01-04T00:00:00.000Z', + toDate: '2025-01-06T00:00:00.000Z', + count: 50 // Not full - last group + } + } + } as any) + + // Message with date after all groups + const messageDate = new Date('2025-01-10T12:00:00.000Z') + const group = await blob.getMessageGroupByDate(cardId, messageDate, false) + + expect(group).toBeDefined() + expect(group?.blobId).toBe('blob-2') + expect(group?.count).toBeLessThan(mockMetadata.messagesPerBlob) + }) + + it('should handle exact boundary dates correctly', async () => { + mockClient.getJson.mockResolvedValue({ + status: 200, + body: { + 'blob-1': { + cardId, + blobId: 'blob-1' as BlobID, + fromDate: '2025-01-01T00:00:00.000Z', + toDate: '2025-01-05T23:59:59.999Z', + count: 50 + } + } + } as any) + + // Message exactly at the toDate boundary + const messageDate = new Date('2025-01-05T23:59:59.999Z') + const group = await blob.getMessageGroupByDate(cardId, messageDate, false) + + expect(group).toBeDefined() + expect(group?.blobId).toBe('blob-1') + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/client.test.ts b/foundations/communication/packages/server/src/__tests__/client.test.ts new file mode 100644 index 0000000000..5e75e4f7af --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/client.test.ts @@ -0,0 +1,458 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MeasureContext, WorkspaceUuid, PersonUuid, Account } from '@hcengineering/core' +import { HulylakeWorkspaceClient, getWorkspaceClient } from '@hcengineering/hulylake-client' +import { getClient as getAccountClient } from '@hcengineering/account-client' +import { loadMessages } from '@hcengineering/communication-shared' +import { + CardID, + MessageID, + MessageMeta, + Message, + SocialID, + FindMessagesOptions, + BlobID +} from '@hcengineering/communication-types' +import { DbAdapter } from '@hcengineering/communication-sdk-types' +import { LowLevelClient } from '../client' +import { Blob } from '../blob' +import { Metadata } from '../types' + +// Mock dependencies +jest.mock('@hcengineering/hulylake-client') +jest.mock('@hcengineering/account-client') +jest.mock('@hcengineering/communication-shared') +jest.mock('@hcengineering/server-token', () => ({ + generateToken: jest.fn(() => 'mock-token') +})) + +describe('LowLevelClient', () => { + let client: LowLevelClient + let mockDbAdapter: jest.Mocked + let mockBlob: jest.Mocked + let mockMetadata: Metadata + let mockLakeClient: jest.Mocked + let mockAccountClient: any + let mockCtx: jest.Mocked + let mockAccount: Account + + const workspace = 'test-workspace' as WorkspaceUuid + const cardId = 'test-card-id' as CardID + const messageId = 'test-message-id' as MessageID + const blobId = 'test-blob-id' as BlobID + const socialId = 'social-id-123' as SocialID + const personUuid = 'person-uuid-123' as PersonUuid + + beforeEach(() => { + // Reset all mocks + jest.clearAllMocks() + + // Mock DbAdapter + mockDbAdapter = { + findMessagesMeta: jest.fn(), + removeMessageMeta: jest.fn() + } as any + + // Mock Blob + mockBlob = {} as any + + // Mock Metadata + mockMetadata = { + accountsUrl: 'http://accounts-url', + hulylakeUrl: 'http://hulylake-url', + secret: 'test-secret', + messagesPerBlob: 100 + } + + // Mock HulylakeWorkspaceClient + mockLakeClient = { + find: jest.fn(), + update: jest.fn() + } as any + + // Mock getWorkspaceClient + ;(getWorkspaceClient as jest.Mock).mockReturnValue(mockLakeClient) + + // Mock AccountClient + mockAccountClient = { + findPersonBySocialId: jest.fn() + } + ;(getAccountClient as jest.Mock).mockReturnValue(mockAccountClient) + + // Mock loadMessages + ;(loadMessages as jest.Mock).mockResolvedValue([]) + + // Mock MeasureContext + mockCtx = { + warn: jest.fn(), + error: jest.fn(), + info: jest.fn() + } as any + + // Mock Account + mockAccount = { + uuid: personUuid, + socialIds: [] + } as any + + // Create client instance + client = new LowLevelClient(mockDbAdapter, mockBlob, mockMetadata, workspace) + }) + + describe('constructor', () => { + it('should initialize LowLevelClient with correct parameters', () => { + expect(client.db).toBe(mockDbAdapter) + expect(client.blob).toBe(mockBlob) + expect(getWorkspaceClient).toHaveBeenCalledWith(mockMetadata.hulylakeUrl, workspace, 'mock-token') + }) + }) + + describe('findMessage', () => { + it('should return undefined when message meta is not found', async () => { + mockDbAdapter.findMessagesMeta.mockResolvedValue([]) + + const result = await client.findMessage(cardId, messageId) + + expect(result).toBeUndefined() + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledWith({ + cardId, + id: messageId + }) + expect(loadMessages).not.toHaveBeenCalled() + }) + + it('should return message when meta is found', async () => { + const mockMeta: MessageMeta = { + cardId, + id: messageId, + blobId, + createdOn: Date.now() + } as any + + const mockMessage: Message = { + cardId, + id: messageId, + createdOn: Date.now(), + createdBy: personUuid + } as any + + mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) + ;(loadMessages as jest.Mock).mockResolvedValue([mockMessage]) + + const result = await client.findMessage(cardId, messageId) + + expect(result).toEqual(mockMessage) + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledWith({ + cardId, + id: messageId + }) + expect(loadMessages).toHaveBeenCalledWith(mockLakeClient, blobId, { cardId, id: messageId }, undefined) + }) + + it('should pass options to loadMessages', async () => { + const mockMeta: MessageMeta = { + cardId, + id: messageId, + blobId, + createdOn: Date.now() + } as any + + const options: FindMessagesOptions = { + attachments: true + } + + mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) + ;(loadMessages as jest.Mock).mockResolvedValue([]) + + await client.findMessage(cardId, messageId, options) + + expect(loadMessages).toHaveBeenCalledWith(mockLakeClient, blobId, { cardId, id: messageId }, options) + }) + + it('should use cached message meta on subsequent calls', async () => { + const mockMeta: MessageMeta = { + cardId, + id: messageId, + blobId, + createdOn: Date.now() + } as any + + mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) + ;(loadMessages as jest.Mock).mockResolvedValue([]) + + // First call + await client.findMessage(cardId, messageId) + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(1) + + // Second call - should use cache + await client.findMessage(cardId, messageId) + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(1) + }) + }) + + describe('findPersonUuid', () => { + it('should return account uuid if socialId is in account socialIds', async () => { + mockAccount.socialIds = [socialId] + + const result = await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) + + expect(result).toBe(personUuid) + expect(mockAccountClient.findPersonBySocialId).not.toHaveBeenCalled() + }) + + it('should return cached person uuid if available', async () => { + // First call to populate cache + mockAccountClient.findPersonBySocialId.mockResolvedValue(personUuid) + + await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) + + expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledTimes(1) + + // Second call - should use cache + const result = await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) + + expect(result).toBe(personUuid) + expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledTimes(1) + }) + + it('should return undefined if accountsUrl is empty', async () => { + const clientWithoutUrl = new LowLevelClient( + mockDbAdapter, + mockBlob, + { ...mockMetadata, accountsUrl: '' }, + workspace + ) + + const result = await clientWithoutUrl.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) + + expect(result).toBeUndefined() + expect(mockAccountClient.findPersonBySocialId).not.toHaveBeenCalled() + }) + + it('should fetch person uuid from account client', async () => { + mockAccountClient.findPersonBySocialId.mockResolvedValue(personUuid) + + const result = await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) + + expect(result).toBe(personUuid) + expect(getAccountClient).toHaveBeenCalledWith(mockMetadata.accountsUrl, 'mock-token') + expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledWith(socialId, false) + }) + + it('should pass requireAccount parameter to account client', async () => { + mockAccountClient.findPersonBySocialId.mockResolvedValue(personUuid) + + await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId, true) + + expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledWith(socialId, true) + }) + + it('should cache person uuid when found', async () => { + mockAccountClient.findPersonBySocialId.mockResolvedValue(personUuid) + + await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) + + // Second call should use cache + await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) + + expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledTimes(1) + }) + + it('should not cache when person uuid is undefined', async () => { + mockAccountClient.findPersonBySocialId.mockResolvedValue(undefined) + + await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) + + // Second call should try again + await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) + + expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledTimes(2) + }) + + it('should handle errors and log warning', async () => { + const error = new Error('Network error') + mockAccountClient.findPersonBySocialId.mockRejectedValue(error) + + const result = await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) + + expect(result).toBeUndefined() + expect(mockCtx.warn).toHaveBeenCalledWith('Cannot find person uuid', { + socialString: socialId, + err: error + }) + }) + }) + + describe('getMessageMeta', () => { + it('should return cached message meta if available', async () => { + const mockMeta: MessageMeta = { + cardId, + id: messageId, + blobId, + createdOn: Date.now() + } as any + + mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) + + // First call + const result1 = await client.getMessageMeta(cardId, messageId) + expect(result1).toEqual(mockMeta) + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(1) + + // Second call - should use cache + const result2 = await client.getMessageMeta(cardId, messageId) + expect(result2).toEqual(mockMeta) + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(1) + }) + + it('should fetch message meta from db if not cached', async () => { + const mockMeta: MessageMeta = { + cardId, + id: messageId, + blobId, + createdOn: Date.now() + } as any + + mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) + + const result = await client.getMessageMeta(cardId, messageId) + + expect(result).toEqual(mockMeta) + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledWith({ + cardId, + id: messageId + }) + }) + + it('should return undefined if message meta not found', async () => { + mockDbAdapter.findMessagesMeta.mockResolvedValue([]) + + const result = await client.getMessageMeta(cardId, messageId) + + expect(result).toBeUndefined() + }) + + it('should cache message meta after fetching', async () => { + const mockMeta: MessageMeta = { + cardId, + id: messageId, + blobId, + createdOn: Date.now() + } as any + + mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) + + await client.getMessageMeta(cardId, messageId) + await client.getMessageMeta(cardId, messageId) + + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(1) + }) + + it('should handle different cardId and messageId combinations separately', async () => { + const cardId2 = 'card-2' as CardID + const messageId2 = 'message-2' as MessageID + + const mockMeta1: MessageMeta = { + cardId, + id: messageId, + blobId, + createdOn: Date.now() + } as any + + const mockMeta2: MessageMeta = { + cardId: cardId2, + id: messageId2, + blobId: 'blob-2' as BlobID, + createdOn: Date.now() + } as any + + mockDbAdapter.findMessagesMeta.mockResolvedValueOnce([mockMeta1]).mockResolvedValueOnce([mockMeta2]) + + const result1 = await client.getMessageMeta(cardId, messageId) + const result2 = await client.getMessageMeta(cardId2, messageId2) + + expect(result1).toEqual(mockMeta1) + expect(result2).toEqual(mockMeta2) + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(2) + }) + }) + + describe('removeMessageMeta', () => { + it('should remove message meta from db and cache', async () => { + const mockMeta: MessageMeta = { + cardId, + id: messageId, + blobId, + createdOn: Date.now() + } as any + + // First, add to cache + mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) + await client.getMessageMeta(cardId, messageId) + + // Remove + await client.removeMessageMeta(cardId, messageId) + + expect(mockDbAdapter.removeMessageMeta).toHaveBeenCalledWith(cardId, messageId) + + // Verify cache is cleared by checking if db is called again + mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) + await client.getMessageMeta(cardId, messageId) + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(2) + }) + + it('should call db removeMessageMeta even if not in cache', async () => { + await client.removeMessageMeta(cardId, messageId) + + expect(mockDbAdapter.removeMessageMeta).toHaveBeenCalledWith(cardId, messageId) + }) + + it('should only remove specific message from cache', async () => { + const cardId2 = 'card-2' as CardID + const messageId2 = 'message-2' as MessageID + + const mockMeta1: MessageMeta = { + cardId, + id: messageId, + blobId, + createdOn: Date.now() + } as any + + const mockMeta2: MessageMeta = { + cardId: cardId2, + id: messageId2, + blobId: 'blob-2' as BlobID, + createdOn: Date.now() + } as any + + // Add both to cache + mockDbAdapter.findMessagesMeta.mockResolvedValueOnce([mockMeta1]).mockResolvedValueOnce([mockMeta2]) + + await client.getMessageMeta(cardId, messageId) + await client.getMessageMeta(cardId2, messageId2) + + // Remove first one + await client.removeMessageMeta(cardId, messageId) + + // First should be removed from cache + mockDbAdapter.findMessagesMeta.mockResolvedValueOnce([mockMeta1]) + await client.getMessageMeta(cardId, messageId) + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(3) + + // Second should still be cached + await client.getMessageMeta(cardId2, messageId2) + expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(3) + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/error.test.ts b/foundations/communication/packages/server/src/__tests__/error.test.ts new file mode 100644 index 0000000000..bba6553b99 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/error.test.ts @@ -0,0 +1,307 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ApiError } from '../error' + +describe('ApiError', () => { + describe('badRequest', () => { + it('should create an error with 400 status code', () => { + const message = 'Invalid input' + const error = ApiError.badRequest(message) + + expect(error).toBeInstanceOf(ApiError) + expect(error).toBeInstanceOf(Error) + expect(error.code).toBe(400) + expect(error.message).toBe(`Bad Request: ${message}`) + }) + + it('should include the custom message in the error message', () => { + const customMessage = 'Missing required field: userId' + const error = ApiError.badRequest(customMessage) + + expect(error.message).toContain(customMessage) + expect(error.message).toBe(`Bad Request: ${customMessage}`) + }) + + it('should have correct prototype chain', () => { + const error = ApiError.badRequest('test') + + expect(Object.getPrototypeOf(error)).toBe(ApiError.prototype) + }) + }) + + describe('forbidden', () => { + it('should create an error with 403 status code', () => { + const message = 'Access denied' + const error = ApiError.forbidden(message) + + expect(error).toBeInstanceOf(ApiError) + expect(error).toBeInstanceOf(Error) + expect(error.code).toBe(403) + expect(error.message).toBe(`Forbidden: ${message}`) + }) + + it('should include the custom message in the error message', () => { + const customMessage = 'Insufficient permissions' + const error = ApiError.forbidden(customMessage) + + expect(error.message).toContain(customMessage) + expect(error.message).toBe(`Forbidden: ${customMessage}`) + }) + + it('should have correct prototype chain', () => { + const error = ApiError.forbidden('test') + + expect(Object.getPrototypeOf(error)).toBe(ApiError.prototype) + }) + }) + + describe('notFound', () => { + it('should create an error with 404 status code', () => { + const message = 'Resource not found' + const error = ApiError.notFound(message) + + expect(error).toBeInstanceOf(ApiError) + expect(error).toBeInstanceOf(Error) + expect(error.code).toBe(404) + expect(error.message).toBe(`Not Found: ${message}`) + }) + + it('should include the custom message in the error message', () => { + const customMessage = 'User with id 123 not found' + const error = ApiError.notFound(customMessage) + + expect(error.message).toContain(customMessage) + expect(error.message).toBe(`Not Found: ${customMessage}`) + }) + + it('should have correct prototype chain', () => { + const error = ApiError.notFound('test') + + expect(Object.getPrototypeOf(error)).toBe(ApiError.prototype) + }) + }) + + describe('toJSON', () => { + it('should return object with code and message for badRequest', () => { + const message = 'Invalid data' + const error = ApiError.badRequest(message) + const json = error.toJSON() + + expect(json).toEqual({ + code: 400, + message: `Bad Request: ${message}` + }) + }) + + it('should return object with code and message for forbidden', () => { + const message = 'Access denied' + const error = ApiError.forbidden(message) + const json = error.toJSON() + + expect(json).toEqual({ + code: 403, + message: `Forbidden: ${message}` + }) + }) + + it('should return object with code and message for notFound', () => { + const message = 'Resource missing' + const error = ApiError.notFound(message) + const json = error.toJSON() + + expect(json).toEqual({ + code: 404, + message: `Not Found: ${message}` + }) + }) + + it('should return a plain object', () => { + const error = ApiError.badRequest('test') + const json = error.toJSON() + + expect(typeof json).toBe('object') + expect(json.constructor).toBe(Object) + }) + }) + + describe('toString', () => { + it('should return JSON string representation for badRequest', () => { + const message = 'Invalid input' + const error = ApiError.badRequest(message) + const stringified = error.toString() + + expect(stringified).toBe( + JSON.stringify({ + code: 400, + message: `Bad Request: ${message}` + }) + ) + }) + + it('should return JSON string representation for forbidden', () => { + const message = 'No access' + const error = ApiError.forbidden(message) + const stringified = error.toString() + + expect(stringified).toBe( + JSON.stringify({ + code: 403, + message: `Forbidden: ${message}` + }) + ) + }) + + it('should return JSON string representation for notFound', () => { + const message = 'Page not found' + const error = ApiError.notFound(message) + const stringified = error.toString() + + expect(stringified).toBe( + JSON.stringify({ + code: 404, + message: `Not Found: ${message}` + }) + ) + }) + + it('should return valid JSON string', () => { + const error = ApiError.badRequest('test') + const stringified = error.toString() + + expect(() => JSON.parse(stringified)).not.toThrow() + const parsed = JSON.parse(stringified) + expect(parsed).toHaveProperty('code') + expect(parsed).toHaveProperty('message') + }) + }) + + describe('Error behavior', () => { + it('should be catchable as Error', () => { + try { + throw ApiError.badRequest('test error') + } catch (err) { + expect(err).toBeInstanceOf(Error) + expect(err).toBeInstanceOf(ApiError) + } + }) + + it('should preserve error message in stack trace', () => { + const message = 'Test error message' + const error = ApiError.badRequest(message) + + expect(error.stack).toBeDefined() + expect(error.stack).toContain('Bad Request: Test error message') + }) + + it('should have name property inherited from Error', () => { + const error = ApiError.badRequest('test') + + expect(error.name).toBe('Error') + }) + + it('should allow instanceof checks', () => { + const badRequestError = ApiError.badRequest('test') + const forbiddenError = ApiError.forbidden('test') + const notFoundError = ApiError.notFound('test') + + expect(badRequestError instanceof ApiError).toBe(true) + expect(forbiddenError instanceof ApiError).toBe(true) + expect(notFoundError instanceof ApiError).toBe(true) + expect(badRequestError instanceof Error).toBe(true) + expect(forbiddenError instanceof Error).toBe(true) + expect(notFoundError instanceof Error).toBe(true) + }) + }) + + describe('Edge cases', () => { + it('should handle empty string message', () => { + const error = ApiError.badRequest('') + + expect(error.message).toBe('Bad Request: ') + expect(error.code).toBe(400) + }) + + it('should handle special characters in message', () => { + const message = 'Error with "quotes" and \'apostrophes\' and \n newlines' + const error = ApiError.notFound(message) + + expect(error.message).toContain(message) + }) + + it('should handle unicode characters in message', () => { + const message = 'Ошибка с юникодом 🚀 и эмодзи' + const error = ApiError.forbidden(message) + + expect(error.message).toContain(message) + expect((error.toJSON() as any).message).toContain(message) + }) + + it('should handle very long messages', () => { + const longMessage = 'A'.repeat(1000) + const error = ApiError.badRequest(longMessage) + + expect(error.message).toContain(longMessage) + expect(error.message.length).toBeGreaterThan(1000) + }) + }) + + describe('Serialization', () => { + it('should be JSON.stringify compatible', () => { + const error = ApiError.badRequest('test') + + expect(() => JSON.stringify(error)).not.toThrow() + }) + + it('should produce same result from toJSON and JSON.stringify', () => { + const error = ApiError.forbidden('test') + const jsonObject = error.toJSON() + const stringified = JSON.stringify(jsonObject) + + expect(stringified).toBe(error.toString()) + }) + + it('should maintain data integrity through serialization', () => { + const message = 'Original message' + const error = ApiError.notFound(message) + const serialized = error.toString() + const deserialized = JSON.parse(serialized) + + expect(deserialized.code).toBe(404) + expect(deserialized.message).toBe(`Not Found: ${message}`) + }) + }) + + describe('Different error types comparison', () => { + it('should create distinct errors with different codes', () => { + const badRequest = ApiError.badRequest('test') + const forbidden = ApiError.forbidden('test') + const notFound = ApiError.notFound('test') + + expect(badRequest.code).not.toBe(forbidden.code) + expect(badRequest.code).not.toBe(notFound.code) + expect(forbidden.code).not.toBe(notFound.code) + }) + + it('should create distinct messages with different prefixes', () => { + const message = 'same message' + const badRequest = ApiError.badRequest(message) + const forbidden = ApiError.forbidden(message) + const notFound = ApiError.notFound(message) + + expect(badRequest.message).toContain('Bad Request') + expect(forbidden.message).toContain('Forbidden') + expect(notFound.message).toContain('Not Found') + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/index.test.ts b/foundations/communication/packages/server/src/__tests__/index.test.ts new file mode 100644 index 0000000000..36b331d037 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/index.test.ts @@ -0,0 +1,478 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' +import { + CardID, + Collaborator, + FindCollaboratorsParams, + FindLabelsParams, + FindMessagesGroupParams, + FindMessagesMetaParams, + FindNotificationContextParams, + FindNotificationsParams, + FindPeersParams, + Label, + MessageMeta, + MessagesGroup, + Notification, + NotificationContext, + Peer +} from '@hcengineering/communication-types' +import { Event, EventResult, SessionData } from '@hcengineering/communication-sdk-types' +import { createDbAdapter } from '@hcengineering/communication-cockroach' +import { Api } from '../index' +import { getMetadata } from '../metadata' +import { buildMiddlewares } from '../middlewares' +import { Blob } from '../blob' +import { LowLevelClient } from '../client' + +// Mock dependencies +jest.mock('@hcengineering/communication-cockroach') +jest.mock('../metadata') +jest.mock('../middlewares') +jest.mock('../blob') +jest.mock('../client') + +describe('Api', () => { + let mockCtx: jest.Mocked + let mockMiddlewares: any + let mockSession: SessionData + let mockDbAdapter: any + let mockBlob: jest.Mocked + let mockClient: jest.Mocked + let mockMetadata: any + + const workspace = 'test-workspace' as WorkspaceUuid + const dbUrl = 'postgresql://test-db' + const cardId = 'test-card-id' as CardID + + const createMockCallbacks = (): { registerAsyncRequest: jest.Mock, broadcast: jest.Mock, enqueue: jest.Mock } => ({ + registerAsyncRequest: jest.fn(), + broadcast: jest.fn(), + enqueue: jest.fn() + }) + + beforeEach(() => { + jest.clearAllMocks() + + // Mock MeasureContext + mockCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis() + } as any + + // Mock SessionData + mockSession = { + sessionId: 'session-123', + account: { + uuid: 'account-uuid', + socialIds: [] + } + } as any + + // Mock metadata + mockMetadata = { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'test-secret', + messagesPerBlob: 200 + } + ;(getMetadata as jest.Mock).mockReturnValue(mockMetadata) + + // Mock DbAdapter + mockDbAdapter = { + findMessagesMeta: jest.fn(), + close: jest.fn() + } + ;(createDbAdapter as jest.Mock).mockResolvedValue(mockDbAdapter) + + // Mock Blob + mockBlob = { + createMessage: jest.fn() + } as any + ;(Blob as jest.Mock).mockImplementation(() => mockBlob) + + // Mock LowLevelClient + mockClient = { + db: mockDbAdapter, + blob: mockBlob + } as any + ;(LowLevelClient as jest.Mock).mockImplementation(() => mockClient) + + // Mock middlewares + mockMiddlewares = { + findMessagesMeta: jest.fn(), + findMessagesGroups: jest.fn(), + findNotificationContexts: jest.fn(), + findNotifications: jest.fn(), + findLabels: jest.fn(), + findCollaborators: jest.fn(), + findPeers: jest.fn(), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + event: jest.fn(), + closeSession: jest.fn(), + close: jest.fn() + } + ;(buildMiddlewares as jest.Mock).mockResolvedValue(mockMiddlewares) + }) + + describe('create', () => { + it('should create Api instance with all dependencies', async () => { + const callbacks = createMockCallbacks() + const api = await Api.create(mockCtx, workspace, dbUrl, callbacks) + + expect(api).toBeInstanceOf(Api) + expect(getMetadata).toHaveBeenCalled() + expect(createDbAdapter).toHaveBeenCalledWith(dbUrl, workspace, mockCtx, { withLogs: false }) + expect(Blob).toHaveBeenCalledWith(mockCtx, workspace, mockMetadata) + expect(LowLevelClient).toHaveBeenCalledWith(mockDbAdapter, mockBlob, mockMetadata, workspace) + expect(buildMiddlewares).toHaveBeenCalled() + }) + + it('should initialize middlewares with correct context', async () => { + const callbacks = createMockCallbacks() + await Api.create(mockCtx, workspace, dbUrl, callbacks) + + expect(buildMiddlewares).toHaveBeenCalledWith(mockCtx, workspace, mockMetadata, mockClient, callbacks) + }) + + it('should handle callback functions', async () => { + const callbacks = { + onCardUpdated: jest.fn(), + onMessageCreated: jest.fn(), + registerAsyncRequest: jest.fn(), + broadcast: jest.fn(), + enqueue: jest.fn() + } + + await Api.create(mockCtx, workspace, dbUrl, callbacks) + + expect(buildMiddlewares).toHaveBeenCalled() + }) + }) + + describe('findMessagesMeta', () => { + it('should delegate to middlewares.findMessagesMeta', async () => { + const params: FindMessagesMetaParams = { cardId } + const expectedResult: MessageMeta[] = [ + { cardId, id: 'msg-1' as any, blobId: 'blob-1' as any, createdOn: Date.now() } + ] as any + + mockMiddlewares.findMessagesMeta.mockResolvedValue(expectedResult) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + const result = await api.findMessagesMeta(mockSession, params) + + expect(result).toEqual(expectedResult) + expect(mockMiddlewares.findMessagesMeta).toHaveBeenCalledWith(mockSession, params) + }) + }) + + describe('findMessagesGroups', () => { + it('should delegate to middlewares.findMessagesGroups', async () => { + const params: FindMessagesGroupParams = { cardId } + const expectedResult: MessagesGroup[] = [{ cardId, blobId: 'blob-1' as any, count: 10 }] as any + + mockMiddlewares.findMessagesGroups.mockResolvedValue(expectedResult) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + const result = await api.findMessagesGroups(mockSession, params) + + expect(result).toEqual(expectedResult) + expect(mockMiddlewares.findMessagesGroups).toHaveBeenCalledWith(mockSession, params) + }) + }) + + describe('findNotificationContexts', () => { + it('should delegate to middlewares.findNotificationContexts without subscription', async () => { + const params: FindNotificationContextParams = { limit: 10 } + const expectedResult: NotificationContext[] = [{ id: 'ctx-1', cardId, unreadCount: 5 }] as any + + mockMiddlewares.findNotificationContexts.mockResolvedValue(expectedResult) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + const result = await api.findNotificationContexts(mockSession, params) + + expect(result).toEqual(expectedResult) + expect(mockMiddlewares.findNotificationContexts).toHaveBeenCalledWith(mockSession, params, undefined) + }) + + it('should pass subscription to middlewares.findNotificationContexts', async () => { + const params: FindNotificationContextParams = { limit: 10 } + const subscription = 'sub-123' + const expectedResult: NotificationContext[] = [] + + mockMiddlewares.findNotificationContexts.mockResolvedValue(expectedResult) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + await api.findNotificationContexts(mockSession, params, subscription) + + expect(mockMiddlewares.findNotificationContexts).toHaveBeenCalledWith(mockSession, params, subscription) + }) + }) + + describe('findNotifications', () => { + it('should delegate to middlewares.findNotifications without subscription', async () => { + const params: FindNotificationsParams = { cardId } + const expectedResult: Notification[] = [{ id: 'notif-1', cardId, isRead: false }] as any + + mockMiddlewares.findNotifications.mockResolvedValue(expectedResult) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + const result = await api.findNotifications(mockSession, params) + + expect(result).toEqual(expectedResult) + expect(mockMiddlewares.findNotifications).toHaveBeenCalledWith(mockSession, params, undefined) + }) + + it('should pass subscription to middlewares.findNotifications', async () => { + const params: FindNotificationsParams = { cardId } + const subscription = 'sub-456' + const expectedResult: Notification[] = [] + + mockMiddlewares.findNotifications.mockResolvedValue(expectedResult) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + await api.findNotifications(mockSession, params, subscription) + + expect(mockMiddlewares.findNotifications).toHaveBeenCalledWith(mockSession, params, subscription) + }) + }) + + describe('findLabels', () => { + it('should delegate to middlewares.findLabels', async () => { + const params: FindLabelsParams = { cardId } + const expectedResult: Label[] = [{ id: 'label-1', name: 'Bug', color: 'red' }] as any + + mockMiddlewares.findLabels.mockResolvedValue(expectedResult) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + const result = await api.findLabels(mockSession, params) + + expect(result).toEqual(expectedResult) + expect(mockMiddlewares.findLabels).toHaveBeenCalledWith(mockSession, params) + }) + }) + + describe('findCollaborators', () => { + it('should delegate to middlewares.findCollaborators', async () => { + const params: FindCollaboratorsParams = { cardId } + const expectedResult: Collaborator[] = [{ personUuid: 'person-1' as any, role: 'owner' }] as any + + mockMiddlewares.findCollaborators.mockResolvedValue(expectedResult) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + const result = await api.findCollaborators(mockSession, params) + + expect(result).toEqual(expectedResult) + expect(mockMiddlewares.findCollaborators).toHaveBeenCalledWith(mockSession, params) + }) + }) + + describe('findPeers', () => { + it('should delegate to middlewares.findPeers', async () => { + const params: FindPeersParams = { cardId } + const expectedResult: Peer[] = [{ personUuid: 'person-1' as any, name: 'John Doe' }] as any + + mockMiddlewares.findPeers.mockResolvedValue(expectedResult) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + const result = await api.findPeers(mockSession, params) + + expect(result).toEqual(expectedResult) + expect(mockMiddlewares.findPeers).toHaveBeenCalledWith(mockSession, params) + }) + }) + + describe('subscribeCard', () => { + it('should delegate to middlewares.subscribeCard', async () => { + const subscription = 'sub-789' + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + api.subscribeCard(mockSession, cardId, subscription) + + expect(mockMiddlewares.subscribeCard).toHaveBeenCalledWith(mockSession, cardId, subscription) + }) + + it('should not return a value', async () => { + const subscription = 'sub-789' + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + api.subscribeCard(mockSession, cardId, subscription) + + expect(mockMiddlewares.subscribeCard).toHaveBeenCalled() + }) + }) + + describe('unsubscribeCard', () => { + it('should delegate to middlewares.unsubscribeCard', async () => { + const subscription = 'sub-999' + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + api.unsubscribeCard(mockSession, cardId, subscription) + + expect(mockMiddlewares.unsubscribeCard).toHaveBeenCalledWith(mockSession, cardId, subscription) + }) + + it('should not return a value', async () => { + const subscription = 'sub-999' + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + api.unsubscribeCard(mockSession, cardId, subscription) + + expect(mockMiddlewares.unsubscribeCard).toHaveBeenCalled() + }) + }) + + describe('event', () => { + it('should delegate to middlewares.event', async () => { + const event: Event = { + type: 'message.create', + data: { cardId, content: 'Test message' } + } as any + + const expectedResult: EventResult = { + success: true + } as any + + mockMiddlewares.event.mockResolvedValue(expectedResult) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + const result = await api.event(mockSession, event) + + expect(result).toEqual(expectedResult) + expect(mockMiddlewares.event).toHaveBeenCalledWith(mockSession, event) + }) + + it('should handle different event types', async () => { + const events: Event[] = [ + { type: 'message.update', data: {} }, + { type: 'message.delete', data: {} }, + { type: 'notification.read', data: {} } + ] as any + + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + for (const event of events) { + mockMiddlewares.event.mockResolvedValue({ success: true }) + await api.event(mockSession, event) + expect(mockMiddlewares.event).toHaveBeenCalledWith(mockSession, event) + } + }) + }) + + describe('closeSession', () => { + it('should delegate to middlewares.closeSession', async () => { + const sessionId = 'session-to-close' + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + await api.closeSession(sessionId) + + expect(mockMiddlewares.closeSession).toHaveBeenCalledWith(sessionId) + }) + + it('should complete without errors', async () => { + const sessionId = 'session-123' + mockMiddlewares.closeSession.mockResolvedValue(undefined) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + await expect(api.closeSession(sessionId)).resolves.toBeUndefined() + }) + }) + + describe('close', () => { + it('should delegate to middlewares.close', async () => { + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + await api.close() + + expect(mockMiddlewares.close).toHaveBeenCalled() + }) + + it('should complete without errors', async () => { + mockMiddlewares.close.mockResolvedValue(undefined) + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + await expect(api.close()).resolves.toBeUndefined() + }) + }) + + describe('Error handling', () => { + it('should propagate errors from middlewares', async () => { + const error = new Error('Middleware error') + mockMiddlewares.findMessagesMeta.mockRejectedValue(error) + + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + const params: FindMessagesMetaParams = { cardId } + + await expect(api.findMessagesMeta(mockSession, params)).rejects.toThrow(error) + }) + + it('should handle errors during creation', async () => { + const error = new Error('DB connection failed') + ;(createDbAdapter as jest.Mock).mockRejectedValue(error) + + await expect(Api.create(mockCtx, workspace, dbUrl, createMockCallbacks())).rejects.toThrow(error) + }) + }) + + describe('Integration scenarios', () => { + it('should handle multiple operations in sequence', async () => { + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + + mockMiddlewares.findMessagesMeta.mockResolvedValue([]) + mockMiddlewares.findLabels.mockResolvedValue([]) + mockMiddlewares.findCollaborators.mockResolvedValue([]) + + await api.findMessagesMeta(mockSession, { cardId }) + await api.findLabels(mockSession, { cardId }) + await api.findCollaborators(mockSession, { cardId }) + + expect(mockMiddlewares.findMessagesMeta).toHaveBeenCalledTimes(1) + expect(mockMiddlewares.findLabels).toHaveBeenCalledTimes(1) + expect(mockMiddlewares.findCollaborators).toHaveBeenCalledTimes(1) + }) + + it('should handle subscription and unsubscription flow', async () => { + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + const subscription = 'sub-flow' + + api.subscribeCard(mockSession, cardId, subscription) + expect(mockMiddlewares.subscribeCard).toHaveBeenCalledWith(mockSession, cardId, subscription) + + api.unsubscribeCard(mockSession, cardId, subscription) + expect(mockMiddlewares.unsubscribeCard).toHaveBeenCalledWith(mockSession, cardId, subscription) + }) + + it('should handle session lifecycle', async () => { + const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) + const sessionId = 'lifecycle-session' + + // Perform operations + mockMiddlewares.findMessagesMeta.mockResolvedValue([]) + await api.findMessagesMeta(mockSession, { cardId }) + + // Close session + await api.closeSession(sessionId) + expect(mockMiddlewares.closeSession).toHaveBeenCalledWith(sessionId) + + // Close API + await api.close() + expect(mockMiddlewares.close).toHaveBeenCalled() + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/messageId.test.ts b/foundations/communication/packages/server/src/__tests__/messageId.test.ts new file mode 100644 index 0000000000..dd622f0b57 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/messageId.test.ts @@ -0,0 +1,93 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { generateMessageId } from '../messageId' + +describe('messageId', () => { + describe('generateMessageId', () => { + it('should generate a valid MessageID', () => { + const id = generateMessageId() + + expect(id).toBeDefined() + expect(typeof id).toBe('string') + expect(id.length).toBeGreaterThan(0) + }) + + it('should generate numeric string IDs', () => { + const id = generateMessageId() + + expect(/^\d+$/.test(id)).toBe(true) + }) + + it('should generate unique IDs on consecutive calls', () => { + const id1 = generateMessageId() + const id2 = generateMessageId() + const id3 = generateMessageId() + + expect(id1).not.toBe(id2) + expect(id2).not.toBe(id3) + expect(id1).not.toBe(id3) + }) + + it('should generate monotonically increasing IDs', () => { + const id1 = BigInt(generateMessageId()) + const id2 = BigInt(generateMessageId()) + const id3 = BigInt(generateMessageId()) + + expect(id2).toBeGreaterThan(id1) + expect(id3).toBeGreaterThan(id2) + }) + + it('should generate IDs in sequence even when called rapidly', () => { + const ids = [] + const count = 100 + + for (let i = 0; i < count; i++) { + ids.push(generateMessageId()) + } + + const uniqueIds = new Set(ids) + expect(uniqueIds.size).toBe(count) + + for (let i = 1; i < ids.length; i++) { + expect(BigInt(ids[i])).toBeGreaterThan(BigInt(ids[i - 1])) + } + }) + + it('should generate IDs that can be parsed as BigInt', () => { + const id = generateMessageId() + + expect(() => BigInt(id)).not.toThrow() + const bigIntId = BigInt(id) + expect(bigIntId).toBeGreaterThan(0n) + }) + + it('should maintain order across multiple calls', () => { + const batch1 = Array.from({ length: 10 }, () => generateMessageId()) + const batch2 = Array.from({ length: 10 }, () => generateMessageId()) + + const lastFromBatch1 = BigInt(batch1[batch1.length - 1]) + const firstFromBatch2 = BigInt(batch2[0]) + + expect(firstFromBatch2).toBeGreaterThan(lastFromBatch1) + }) + + it('should not generate negative IDs', () => { + for (let i = 0; i < 20; i++) { + const id = generateMessageId() + expect(id.startsWith('-')).toBe(false) + expect(BigInt(id)).toBeGreaterThanOrEqual(0n) + } + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/metadata.test.ts b/foundations/communication/packages/server/src/__tests__/metadata.test.ts new file mode 100644 index 0000000000..54166941d2 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/metadata.test.ts @@ -0,0 +1,287 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { getMetadata } from '../metadata' +import { MessageID } from '@hcengineering/communication-types' +import { generateMessageId } from '../messageId' + +describe('metadata', () => { + let originalEnv: NodeJS.ProcessEnv + + beforeEach(() => { + // Save original environment + originalEnv = { ...process.env } + }) + + afterEach(() => { + // Restore original environment + process.env = originalEnv + }) + + describe('getMetadata', () => { + it('should return default values when environment variables are not set', () => { + delete process.env.ACCOUNTS_URL + delete process.env.SERVER_SECRET + delete process.env.HULYLAKE_URL + delete process.env.MESSAGES_PER_BLOB + + const metadata = getMetadata() + + expect(metadata).toEqual({ + accountsUrl: '', + secret: 'secret', + hulylakeUrl: 'http://huly.local:8096', + messagesPerBlob: 200 + }) + }) + + it('should use ACCOUNTS_URL from environment', () => { + process.env.ACCOUNTS_URL = 'http://custom-accounts-url' + + const metadata = getMetadata() + + expect(metadata.accountsUrl).toBe('http://custom-accounts-url') + }) + + it('should use SERVER_SECRET from environment', () => { + process.env.SERVER_SECRET = 'custom-secret-key' + + const metadata = getMetadata() + + expect(metadata.secret).toBe('custom-secret-key') + }) + + it('should use HULYLAKE_URL from environment', () => { + process.env.HULYLAKE_URL = 'http://custom-hulylake:9000' + + const metadata = getMetadata() + + expect(metadata.hulylakeUrl).toBe('http://custom-hulylake:9000') + }) + + it('should use MESSAGES_PER_BLOB from environment', () => { + process.env.MESSAGES_PER_BLOB = '500' + + const metadata = getMetadata() + + expect(metadata.messagesPerBlob).toBe(500) + }) + + it('should use all custom environment variables', () => { + process.env.ACCOUNTS_URL = 'http://accounts' + process.env.SERVER_SECRET = 'my-secret' + process.env.HULYLAKE_URL = 'http://hulylake:8080' + process.env.MESSAGES_PER_BLOB = '1000' + + const metadata = getMetadata() + + expect(metadata).toEqual({ + accountsUrl: 'http://accounts', + secret: 'my-secret', + hulylakeUrl: 'http://hulylake:8080', + messagesPerBlob: 1000 + }) + }) + + it('should handle empty string for ACCOUNTS_URL', () => { + process.env.ACCOUNTS_URL = '' + + const metadata = getMetadata() + + expect(metadata.accountsUrl).toBe('') + }) + + it('should convert MESSAGES_PER_BLOB to number', () => { + process.env.MESSAGES_PER_BLOB = '750' + + const metadata = getMetadata() + + expect(typeof metadata.messagesPerBlob).toBe('number') + expect(metadata.messagesPerBlob).toBe(750) + }) + + it('should handle invalid MESSAGES_PER_BLOB as NaN', () => { + process.env.MESSAGES_PER_BLOB = 'invalid' + + const metadata = getMetadata() + + expect(metadata.messagesPerBlob).toBeNaN() + }) + + it('should return a new object on each call', () => { + const metadata1 = getMetadata() + const metadata2 = getMetadata() + + expect(metadata1).not.toBe(metadata2) + expect(metadata1).toEqual(metadata2) + }) + + it('should have correct type structure', () => { + const metadata = getMetadata() + + expect(metadata).toHaveProperty('accountsUrl') + expect(metadata).toHaveProperty('secret') + expect(metadata).toHaveProperty('hulylakeUrl') + expect(metadata).toHaveProperty('messagesPerBlob') + + expect(typeof metadata.accountsUrl).toBe('string') + expect(typeof metadata.secret).toBe('string') + expect(typeof metadata.hulylakeUrl).toBe('string') + expect(typeof metadata.messagesPerBlob).toBe('number') + }) + + it('should handle zero MESSAGES_PER_BLOB', () => { + process.env.MESSAGES_PER_BLOB = '0' + + const metadata = getMetadata() + + expect(metadata.messagesPerBlob).toBe(0) + }) + + it('should handle negative MESSAGES_PER_BLOB', () => { + process.env.MESSAGES_PER_BLOB = '-100' + + const metadata = getMetadata() + + expect(metadata.messagesPerBlob).toBe(-100) + }) + + it('should handle floating point MESSAGES_PER_BLOB', () => { + process.env.MESSAGES_PER_BLOB = '123.45' + + const metadata = getMetadata() + + expect(metadata.messagesPerBlob).toBe(123.45) + }) + + it('should handle URLs with different protocols', () => { + process.env.ACCOUNTS_URL = 'https://secure-accounts.com' + process.env.HULYLAKE_URL = 'wss://hulylake.example.com' + + const metadata = getMetadata() + + expect(metadata.accountsUrl).toBe('https://secure-accounts.com') + expect(metadata.hulylakeUrl).toBe('wss://hulylake.example.com') + }) + + it('should handle URLs with ports', () => { + process.env.ACCOUNTS_URL = 'http://localhost:3000' + process.env.HULYLAKE_URL = 'http://localhost:8096' + + const metadata = getMetadata() + + expect(metadata.accountsUrl).toBe('http://localhost:3000') + expect(metadata.hulylakeUrl).toBe('http://localhost:8096') + }) + }) +}) + +describe('messageId', () => { + describe('generateMessageId', () => { + it('should generate a valid MessageID', () => { + const id = generateMessageId() + + expect(id).toBeDefined() + expect(typeof id).toBe('string') + expect(id.length).toBeGreaterThan(0) + }) + + it('should generate numeric string IDs', () => { + const id = generateMessageId() + + expect(/^\d+$/.test(id)).toBe(true) + }) + + it('should generate unique IDs on consecutive calls', () => { + const id1 = generateMessageId() + const id2 = generateMessageId() + const id3 = generateMessageId() + + expect(id1).not.toBe(id2) + expect(id2).not.toBe(id3) + expect(id1).not.toBe(id3) + }) + + it('should generate monotonically increasing IDs', () => { + const id1 = BigInt(generateMessageId()) + const id2 = BigInt(generateMessageId()) + const id3 = BigInt(generateMessageId()) + + expect(id2).toBeGreaterThan(id1) + expect(id3).toBeGreaterThan(id2) + }) + + it('should generate IDs in sequence even when called rapidly', () => { + const ids: MessageID[] = [] + const count = 100 + + for (let i = 0; i < count; i++) { + ids.push(generateMessageId()) + } + + // Check all IDs are unique + const uniqueIds = new Set(ids) + expect(uniqueIds.size).toBe(count) + + // Check IDs are monotonically increasing + for (let i = 1; i < ids.length; i++) { + expect(BigInt(ids[i])).toBeGreaterThan(BigInt(ids[i - 1])) + } + }) + + it('should handle concurrent generation correctly', () => { + const ids = new Set() + const iterations = 50 + + for (let i = 0; i < iterations; i++) { + ids.add(generateMessageId()) + } + + expect(ids.size).toBe(iterations) + }) + + it('should generate IDs that can be parsed as BigInt', () => { + const id = generateMessageId() + + expect(() => BigInt(id)).not.toThrow() + const bigIntId = BigInt(id) + expect(bigIntId).toBeGreaterThan(0n) + }) + + it('should maintain increasing order across multiple batches', () => { + const batch1 = Array.from({ length: 10 }, () => generateMessageId()) + const batch2 = Array.from({ length: 10 }, () => generateMessageId()) + + const lastFromBatch1 = BigInt(batch1[batch1.length - 1]) + const firstFromBatch2 = BigInt(batch2[0]) + + expect(firstFromBatch2).toBeGreaterThan(lastFromBatch1) + }) + + it('should generate IDs with reasonable length', () => { + const id = generateMessageId() + + // Should be a reasonable length (not too short, not too long) + expect(id.length).toBeGreaterThan(10) + expect(id.length).toBeLessThan(30) + }) + + it('should not generate negative IDs', () => { + for (let i = 0; i < 20; i++) { + const id = generateMessageId() + expect(id.startsWith('-')).toBe(false) + expect(BigInt(id)).toBeGreaterThanOrEqual(0n) + } + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/base.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/base.test.ts new file mode 100644 index 0000000000..81d5291ed3 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/middleware/base.test.ts @@ -0,0 +1,375 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { type MeasureContext, type WorkspaceUuid } from '@hcengineering/core' +import { CreateMessageEvent, MessageEventType, type SessionData } from '@hcengineering/communication-sdk-types' +import { + type AccountUuid, + CardID, + type CardType, + type ContextID, + type Markdown, + MessageID, + MessageType, + type SocialID +} from '@hcengineering/communication-types' + +import { BaseMiddleware } from '../../middleware/base' +import { type Enriched, type Middleware, type MiddlewareContext } from '../../types' +import { type LowLevelClient } from '../../client' + +describe('BaseMiddleware', () => { + let mockContext: MiddlewareContext + let mockClient: jest.Mocked + let mockMeasureCtx: jest.Mocked + let mockNext: jest.Mocked + let session: SessionData + let middleware: BaseMiddleware + + const workspace = 'test-workspace' as WorkspaceUuid + const accountUuid = 'account-123' as AccountUuid + const socialId = 'social-123' as SocialID + + const basicEvent = { + _id: 'event-123', + _eventExtra: {}, + socialId, + date: new Date() + } as const + + beforeEach(() => { + jest.clearAllMocks() + + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis() + } as any as jest.Mocked + + mockClient = { + db: {}, + blob: {} + } as unknown as jest.Mocked + + mockNext = { + event: jest.fn().mockResolvedValue({ success: true }), + findNotificationContexts: jest.fn().mockResolvedValue([{ id: 'ctx-1' }]), + findNotifications: jest.fn().mockResolvedValue([{ id: 'notif-1' }]), + findLabels: jest.fn().mockResolvedValue([{ labelId: 'label-1' }]), + findCollaborators: jest.fn().mockResolvedValue([{ account: accountUuid }]), + findPeers: jest.fn().mockResolvedValue([{ cardId: 'card-1' }]), + findMessagesMeta: jest.fn().mockResolvedValue([{ messageId: 'msg-1' }]), + findMessagesGroups: jest.fn().mockResolvedValue([{ blobId: 'blob-1' }]), + handleBroadcast: jest.fn(), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + close: jest.fn(), + closeSession: jest.fn() + } as any as jest.Mocked + + mockContext = { + ctx: mockMeasureCtx, + client: mockClient, + workspace, + metadata: { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'secret', + messagesPerBlob: 100 + }, + cadsWithPeers: new Set() + } + + session = { + account: { + uuid: accountUuid, + socialIds: [socialId] + }, + sessionId: 'session-123' + } as any as SessionData + + middleware = new BaseMiddleware(mockContext, mockNext) + }) + + describe('event', () => { + it('should delegate to next middleware', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + messageId: 'msg-123' as MessageID, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown, + socialId + } + + const result = await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + expect(result).toEqual({ success: true }) + }) + + it('should return empty object if no next middleware', async () => { + const middlewareWithoutNext = new BaseMiddleware(mockContext) + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + messageId: 'msg-123' as MessageID, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown, + socialId + } + + const result = await middlewareWithoutNext.event(session, event, false) + + expect(result).toEqual({}) + }) + }) + + describe('findMessagesMeta', () => { + it('should delegate to next middleware', async () => { + const params = { cardId: 'card-123' as CardID } + + const result = await middleware.findMessagesMeta(session, params) + + expect(mockNext.findMessagesMeta).toHaveBeenCalledWith(session, params) + expect(result).toEqual([{ messageId: 'msg-1' }]) + }) + + it('should return empty array if no next middleware', async () => { + const middlewareWithoutNext = new BaseMiddleware(mockContext) + const params = { cardId: 'card-123' as CardID } + + const result = await middlewareWithoutNext.findMessagesMeta(session, params) + + expect(result).toEqual([]) + }) + }) + + describe('findMessagesGroups', () => { + it('should delegate to next middleware', async () => { + const params = { cardId: 'card-123' as CardID } + + const result = await middleware.findMessagesGroups(session, params) + + expect(mockNext.findMessagesGroups).toHaveBeenCalledWith(session, params) + expect(result).toEqual([{ blobId: 'blob-1' }]) + }) + + it('should return empty array if no next middleware', async () => { + const middlewareWithoutNext = new BaseMiddleware(mockContext) + const params = { cardId: 'card-123' as CardID } + + const result = await middlewareWithoutNext.findMessagesGroups(session, params) + + expect(result).toEqual([]) + }) + }) + + describe('findNotificationContexts', () => { + it('should delegate to next middleware', async () => { + const params = { cardId: 'card-123' as CardID, account: accountUuid } + const subscription = 'sub-123' + + const result = await middleware.findNotificationContexts(session, params, subscription) + + expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(session, params, subscription) + expect(result).toEqual([{ id: 'ctx-1' }]) + }) + + it('should return empty array if no next middleware', async () => { + const middlewareWithoutNext = new BaseMiddleware(mockContext) + const params = { cardId: 'card-123' as CardID } + + const result = await middlewareWithoutNext.findNotificationContexts(session, params) + + expect(result).toEqual([]) + }) + }) + + describe('findNotifications', () => { + it('should delegate to next middleware', async () => { + const params = { contextId: 'ctx-123' as ContextID } + const subscription = 'sub-123' + + const result = await middleware.findNotifications(session, params, subscription) + + expect(mockNext.findNotifications).toHaveBeenCalledWith(session, params, subscription) + expect(result).toEqual([{ id: 'notif-1' }]) + }) + + it('should return empty array if no next middleware', async () => { + const middlewareWithoutNext = new BaseMiddleware(mockContext) + const params = { contextId: 'ctx-123' as ContextID } + + const result = await middlewareWithoutNext.findNotifications(session, params) + + expect(result).toEqual([]) + }) + }) + + describe('findLabels', () => { + it('should delegate to next middleware', async () => { + const params = { cardId: 'card-123' as CardID } + const subscription = 'sub-123' + + const result = await middleware.findLabels(session, params, subscription) + + expect(mockNext.findLabels).toHaveBeenCalledWith(session, params, subscription) + expect(result).toEqual([{ labelId: 'label-1' }]) + }) + + it('should return empty array if no next middleware', async () => { + const middlewareWithoutNext = new BaseMiddleware(mockContext) + const params = { cardId: 'card-123' as CardID } + + const result = await middlewareWithoutNext.findLabels(session, params) + + expect(result).toEqual([]) + }) + }) + + describe('findCollaborators', () => { + it('should delegate to next middleware', async () => { + const params = { cardId: 'card-123' as CardID } + + const result = await middleware.findCollaborators(session, params) + + expect(mockNext.findCollaborators).toHaveBeenCalledWith(session, params) + expect(result).toEqual([{ account: accountUuid }]) + }) + + it('should return empty array if no next middleware', async () => { + const middlewareWithoutNext = new BaseMiddleware(mockContext) + const params = { cardId: 'card-123' as CardID } + + const result = await middlewareWithoutNext.findCollaborators(session, params) + + expect(result).toEqual([]) + }) + }) + + describe('findPeers', () => { + it('should delegate to next middleware', async () => { + const params = { workspaceId: workspace, cardId: 'card-123' as CardID } + + const result = await middleware.findPeers(session, params) + + expect(mockNext.findPeers).toHaveBeenCalledWith(session, params) + expect(result).toEqual([{ cardId: 'card-1' }]) + }) + + it('should return empty array if no next middleware', async () => { + const middlewareWithoutNext = new BaseMiddleware(mockContext) + const params = { workspaceId: workspace, cardId: 'card-123' as CardID } + + const result = await middlewareWithoutNext.findPeers(session, params) + + expect(result).toEqual([]) + }) + }) + + describe('handleBroadcast', () => { + it('should delegate to next middleware', () => { + const events: Enriched[] = [ + { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + date: new Date(), + _eventExtra: {} + } + ] + + middleware.handleBroadcast(session, events) + + expect(mockNext.handleBroadcast).toHaveBeenCalledWith(session, events) + }) + + it('should do nothing if no next middleware', () => { + const middlewareWithoutNext = new BaseMiddleware(mockContext) + const events: Enriched[] = [ + { + type: MessageEventType.CreateMessage, + date: new Date(), + _eventExtra: {} + } + ] + + expect(() => { + middlewareWithoutNext.handleBroadcast(session, events) + }).not.toThrow() + }) + }) + + describe('subscribeCard', () => { + it('should delegate to next middleware', () => { + const cardId = 'card-123' as CardID + const subscription = 'sub-123' + + middleware.subscribeCard(session, cardId, subscription) + + expect(mockNext.subscribeCard).toHaveBeenCalledWith(session, cardId, subscription) + }) + + it('should do nothing if no next middleware', () => { + const middlewareWithoutNext = new BaseMiddleware(mockContext) + const cardId = 'card-123' as CardID + const subscription = 'sub-123' + + expect(() => { + middlewareWithoutNext.subscribeCard(session, cardId, subscription) + }).not.toThrow() + }) + }) + + describe('unsubscribeCard', () => { + it('should delegate to next middleware', () => { + const cardId = 'card-123' as CardID + const subscription = 'sub-123' + + middleware.unsubscribeCard(session, cardId, subscription) + + expect(mockNext.unsubscribeCard).toHaveBeenCalledWith(session, cardId, subscription) + }) + + it('should do nothing if no next middleware', () => { + const middlewareWithoutNext = new BaseMiddleware(mockContext) + const cardId = 'card-123' as CardID + const subscription = 'sub-123' + + expect(() => { + middlewareWithoutNext.unsubscribeCard(session, cardId, subscription) + }).not.toThrow() + }) + }) + + describe('close', () => { + it('should call close without errors', () => { + expect(() => { + middleware.close() + }).not.toThrow() + }) + }) + + describe('closeSession', () => { + it('should call closeSession without errors', () => { + expect(() => { + middleware.closeSession('session-123') + }).not.toThrow() + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/broadcast.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/broadcast.test.ts new file mode 100644 index 0000000000..20b1e05151 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/middleware/broadcast.test.ts @@ -0,0 +1,1071 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { type MeasureContext, type WorkspaceUuid } from '@hcengineering/core' +import { + AddCollaboratorsEvent, + AttachmentPatchEvent, + BlobPatchEvent, + CardEventType, + CreateLabelEvent, + CreateMessageEvent, + CreateNotificationContextEvent, + CreateNotificationEvent, + CreatePeerEvent, + LabelEventType, + MessageEventType, + NotificationEventType, + PeerEventType, + ReactionPatchEvent, + RemoveCardEvent, + RemoveCollaboratorsEvent, + RemoveLabelEvent, + RemoveNotificationContextEvent, + RemoveNotificationsEvent, + RemovePatchEvent, + RemovePeerEvent, + type SessionData, + ThreadPatchEvent, + TranslateMessageEvent, + UpdateCardTypeEvent, + UpdateNotificationContextEvent, + UpdateNotificationEvent, + UpdatePatchEvent +} from '@hcengineering/communication-sdk-types' +import { + type AccountUuid, + CardID, + type CardType, + ContextID, + Label, + LabelID, + type Markdown, + MessageType, + NotificationContext, + type SocialID +} from '@hcengineering/communication-types' + +import { BroadcastMiddleware } from '../../middleware/broadcast' +import { type CommunicationCallbacks, type Enriched, Middleware, type MiddlewareContext } from '../../types' +import { type LowLevelClient } from '../../client' + +describe('BroadcastMiddleware', () => { + let mockContext: MiddlewareContext + let mockClient: jest.Mocked + let mockMeasureCtx: jest.Mocked + let mockCallbacks: jest.Mocked + let mockNext: any + let session: SessionData + let session2: SessionData + let middleware: BroadcastMiddleware + + const workspace = 'test-workspace' as WorkspaceUuid + const accountUuid = 'account-123' as AccountUuid + const accountUuid2 = 'account-456' as AccountUuid + const socialId = 'social-123' as SocialID + const socialId2 = 'social-456' as SocialID + const cardId = 'card-123' as CardID + const cardId2 = 'card-456' as CardID + + const basicEvent = { + _id: 'event-123', + _eventExtra: {}, + socialId, + date: new Date() + } as const + + // Helper function to create a complete CreateMessage event for session initialization + const createSessionEvent = (_cardId: CardID = cardId): Enriched => ({ + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: _cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown, + socialId, + date: new Date() + }) + + beforeEach(() => { + jest.clearAllMocks() + + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis(), + contextData: {} + } as any as jest.Mocked + + mockClient = { + db: {}, + blob: {} + } as unknown as jest.Mocked + + mockCallbacks = { + broadcast: jest.fn(), + enqueue: jest.fn(), + registerAsyncRequest: jest.fn() + } as any as jest.Mocked + + mockNext = { + event: jest.fn().mockResolvedValue({}), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + handleBroadcast: jest.fn(), + closeSession: jest.fn(), + close: jest.fn() + } as any as jest.Mocked + + mockContext = { + ctx: mockMeasureCtx, + client: mockClient, + workspace, + metadata: { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'secret', + messagesPerBlob: 100 + }, + cadsWithPeers: new Set() + } + + session = { + account: { + uuid: accountUuid, + socialIds: [socialId] + }, + sessionId: 'session-123', + contextData: {} + } as any as SessionData + + session2 = { + account: { + uuid: accountUuid2, + socialIds: [socialId2] + }, + sessionId: 'session-456', + contextData: {} + } as any as SessionData + + middleware = new BroadcastMiddleware(mockCallbacks, mockContext, mockNext) + }) + + describe('event', () => { + it('should create session and process event', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should handle derived events', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown, + socialId + } + + await middleware.event(session, event, true) + + expect(mockNext.event).toHaveBeenCalledWith(session, event, true) + }) + + it('should create session for multiple users', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown, + socialId + } + + await middleware.event(session, event, false) + await middleware.event(session2, event, false) + + expect(mockNext.event).toHaveBeenCalledTimes(2) + }) + }) + + describe('findNotificationContexts', () => { + it('should create session and find notification contexts', async () => { + const params = { cardId } + const contexts = [{ id: 'ctx-1', cardId, account: accountUuid }] + mockNext.findNotificationContexts.mockResolvedValue(contexts) + + const result = await middleware.findNotificationContexts(session, params) + + expect(result).toEqual(contexts) + expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(session, params, undefined) + }) + + it('should subscribe to contexts cards when subscription provided', async () => { + const params = { cardId } + const subscription = 'sub-123' + const contexts = [ + { id: 'ctx-1' as ContextID, cardId: 'card-1' as CardID, account: accountUuid }, + { id: 'ctx-2' as ContextID, cardId: 'card-2' as CardID, account: accountUuid } + ] + mockNext.findNotificationContexts.mockResolvedValue(contexts) + + await middleware.findNotificationContexts(session, params, subscription) + + expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(session, params, subscription) + }) + + it('should auto-subscribe to all returned context cards', async () => { + const params = { cardId } + const subscription = 'sub-123' + const contexts = [ + { id: 'ctx-1' as ContextID, cardId: 'card-1' as CardID, account: accountUuid }, + { id: 'ctx-2' as ContextID, cardId: 'card-2' as CardID, account: accountUuid }, + { id: 'ctx-3' as ContextID, cardId: 'card-3' as CardID, account: accountUuid } + ] + mockNext.findNotificationContexts.mockResolvedValue(contexts) + + await middleware.findNotificationContexts(session, params, subscription) + + // Verify that subscription tracking is working by checking broadcast behavior + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-1' as CardID, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown + } + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should not subscribe when no subscription provided', async () => { + const params = { cardId } + const contexts = [{ id: 'ctx-1' as ContextID, cardId, account: accountUuid }] + mockNext.findNotificationContexts.mockResolvedValue(contexts) + + await middleware.findNotificationContexts(session, params) + + expect(mockNext.findNotificationContexts).toHaveBeenCalled() + }) + + it('should not subscribe when sessionId is empty', async () => { + const sessionWithoutId: SessionData = { + ...session, + sessionId: '' + } as any + + const params = { cardId } + const subscription = 'sub-123' + const contexts: NotificationContext[] = [{ id: 'ctx-1' as ContextID, cardId, account: accountUuid }] as any[] + mockNext.findNotificationContexts.mockResolvedValue(contexts) + + await middleware.findNotificationContexts(sessionWithoutId, params, subscription) + + expect(mockNext.findNotificationContexts).toHaveBeenCalled() + }) + + it('should not subscribe when sessionId is null', async () => { + const sessionWithoutId: SessionData = { + ...session, + sessionId: null + } as any + + const params = { cardId } + const subscription = 'sub-123' + const contexts: NotificationContext[] = [{ id: 'ctx-1' as ContextID, cardId, account: accountUuid }] as any[] + mockNext.findNotificationContexts.mockResolvedValue(contexts) + + await middleware.findNotificationContexts(sessionWithoutId, params, subscription) + + expect(mockNext.findNotificationContexts).toHaveBeenCalled() + }) + }) + + describe('findNotifications', () => { + it('should create session and find notifications', async () => { + const params = { contextId: 'ctx-123' as ContextID } + const notifications = [{ id: 'notif-1' }] + mockNext.findNotifications.mockResolvedValue(notifications) + + const result = await middleware.findNotifications(session, params) + + expect(result).toEqual(notifications) + expect(mockNext.findNotifications).toHaveBeenCalledWith(session, params, undefined) + }) + + it('should pass query id when provided', async () => { + const params = { contextId: 'ctx-123' as ContextID } + const queryId = 'query-456' + const notifications = [{ id: 'notif-1' }] + mockNext.findNotifications.mockResolvedValue(notifications) + + await middleware.findNotifications(session, params, queryId) + + expect(mockNext.findNotifications).toHaveBeenCalledWith(session, params, queryId) + }) + }) + + describe('findLabels', () => { + it('should create session and find labels', async () => { + const params = { cardId } + const labels: Label[] = [{ labelId: 'label-1' as LabelID, cardId }] as any[] + mockNext.findLabels.mockResolvedValue(labels) + + const result = await middleware.findLabels(session, params) + + expect(result).toEqual(labels) + expect(mockNext.findLabels).toHaveBeenCalledWith(session, params, undefined) + }) + + it('should pass query id when provided', async () => { + const params = { cardId } + const queryId = 'query-789' + mockNext.findLabels.mockResolvedValue([]) + + await middleware.findLabels(session, params, queryId) + + expect(mockNext.findLabels).toHaveBeenCalledWith(session, params, queryId) + }) + }) + + describe('subscribeCard', () => { + it('should track subscription for card', () => { + const subscription = 'sub-123' + + middleware.subscribeCard(session, cardId, subscription) + + expect(() => { + middleware.subscribeCard(session, cardId, subscription) + }).not.toThrow() + }) + + it('should handle multiple subscriptions for same card', () => { + const subscription1 = 'sub-1' + const subscription2 = 'sub-2' + + middleware.subscribeCard(session, cardId, subscription1) + middleware.subscribeCard(session, cardId, subscription2) + + expect(() => { + middleware.subscribeCard(session, cardId, subscription1) + }).not.toThrow() + }) + + it('should handle subscriptions for different cards', () => { + const subscription = 'sub-123' + + middleware.subscribeCard(session, cardId, subscription) + middleware.subscribeCard(session, cardId2, subscription) + + expect(() => { + middleware.subscribeCard(session, cardId, subscription) + }).not.toThrow() + }) + + it('should handle subscription with numeric ID', () => { + const subscription = 12345 + + middleware.subscribeCard(session, cardId, subscription) + + expect(() => { + middleware.subscribeCard(session, cardId, subscription) + }).not.toThrow() + }) + + it('should not subscribe when session has no sessionId', () => { + const sessionWithoutId: SessionData = { + ...session, + sessionId: null + } as any + const subscription = 'sub-123' + + expect(() => { + middleware.subscribeCard(sessionWithoutId, cardId, subscription) + }).not.toThrow() + }) + }) + + describe('unsubscribeCard', () => { + it('should remove subscription from card', () => { + const subscription = 'sub-123' + + middleware.subscribeCard(session, cardId, subscription) + middleware.unsubscribeCard(session, cardId, subscription) + + expect(() => { + middleware.unsubscribeCard(session, cardId, subscription) + }).not.toThrow() + }) + + it('should handle unsubscribe when not subscribed', () => { + const subscription = 'sub-123' + + middleware.unsubscribeCard(session, cardId, subscription) + + expect(() => { + middleware.unsubscribeCard(session, cardId, subscription) + }).not.toThrow() + }) + + it('should handle unsubscribe without sessionId', () => { + const sessionWithoutId: SessionData = { + ...session, + sessionId: null + } as any + const subscription = 'sub-123' + + middleware.unsubscribeCard(sessionWithoutId, cardId, subscription) + + expect(() => { + middleware.unsubscribeCard(sessionWithoutId, cardId, subscription) + }).not.toThrow() + }) + + it('should handle unsubscribe for non-existent session', () => { + const subscription = 'sub-123' + + expect(() => { + middleware.unsubscribeCard(session, cardId, subscription) + }).not.toThrow() + }) + + it('should handle unsubscribe for non-existent card', () => { + const subscription = 'sub-123' + + // Create session first + middleware.subscribeCard(session, cardId, subscription) + + // Try to unsubscribe from different card + expect(() => { + middleware.unsubscribeCard(session, cardId2, subscription) + }).not.toThrow() + }) + + it('should only remove specific subscription', async () => { + const subscription1 = 'sub-1' + const subscription2 = 'sub-2' + + // Create session first + await middleware.event(session, createSessionEvent(cardId), false) + + middleware.subscribeCard(session, cardId, subscription1) + middleware.subscribeCard(session, cardId, subscription2) + middleware.unsubscribeCard(session, cardId, subscription1) + + // Verify subscription2 is still active by checking broadcast + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown + } + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + }) + + describe('handleBroadcast', () => { + it('should call broadcast and enqueue', async () => { + // Create session first + await middleware.event(session, createSessionEvent(cardId), false) + + const events: Enriched[] = [ + { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown + } + ] + + middleware.subscribeCard(session, cardId, 'sub-123') + middleware.handleBroadcast(session, events) + + expect(mockCallbacks.broadcast).toHaveBeenCalled() + expect(mockCallbacks.enqueue).toHaveBeenCalledWith(expect.anything(), events) + }) + + it('should handle empty events array', () => { + middleware.handleBroadcast(session, []) + + expect(mockCallbacks.broadcast).not.toHaveBeenCalled() + expect(mockCallbacks.enqueue).not.toHaveBeenCalled() + }) + + it('should filter events by subscription', async () => { + // Create session first + await middleware.event(session, createSessionEvent(cardId), false) + + middleware.subscribeCard(session, cardId, 'sub-123') + + const subscribedEvent: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown + } + + const unsubscribedEvent: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: cardId2, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown + } + + middleware.handleBroadcast(session, [subscribedEvent, unsubscribedEvent]) + + expect(mockCallbacks.broadcast).toHaveBeenCalled() + const broadcastCall = mockCallbacks.broadcast.mock.calls[0][1] + expect(broadcastCall[session.sessionId ?? '']).toEqual([subscribedEvent]) + }) + + it('should broadcast to multiple sessions', async () => { + // Create sessions first + await middleware.event(session, createSessionEvent(cardId), false) + await middleware.event(session2, createSessionEvent(cardId), false) + + middleware.subscribeCard(session, cardId, 'sub-1') + middleware.subscribeCard(session2, cardId, 'sub-2') + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown + } + + middleware.handleBroadcast(session, [event]) + + expect(mockCallbacks.broadcast).toHaveBeenCalled() + const broadcastCall = mockCallbacks.broadcast.mock.calls[0][1] + expect(broadcastCall[session.sessionId ?? '']).toEqual([event]) + expect(broadcastCall[session2.sessionId ?? '']).toEqual([event]) + }) + + it('should handle MessageEventType.ThreadPatch', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ThreadPatch, + cardId + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle MessageEventType.ReactionPatch', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ReactionPatch, + cardId + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle MessageEventType.BlobPatch', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.BlobPatch, + cardId + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle MessageEventType.AttachmentPatch', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.AttachmentPatch, + cardId + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle MessageEventType.RemovePatch', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.RemovePatch, + cardId + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle MessageEventType.UpdatePatch', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + type: MessageEventType.UpdatePatch, + cardId + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle MessageEventType.TranslateMessage', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + type: MessageEventType.TranslateMessage, + cardId + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle NotificationEventType.CreateNotification for matching account', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + type: NotificationEventType.CreateNotification, + account: accountUuid + } as any + + middleware.handleBroadcast(session, [event]) + + expect(mockCallbacks.broadcast).toHaveBeenCalled() + const broadcastCall = mockCallbacks.broadcast.mock.calls[0][1] + expect(broadcastCall[session.sessionId ?? '']).toEqual([event]) + }) + + it('should not broadcast NotificationEventType.CreateNotification for different account', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + type: NotificationEventType.CreateNotification, + account: accountUuid2 + } as any + + middleware.handleBroadcast(session, [event]) + + // Enqueue is still called + expect(mockCallbacks.enqueue).toHaveBeenCalled() + // No sessions should receive this event (either undefined or empty array) + const broadcastCall = mockCallbacks.broadcast.mock.calls[0]?.[1] + const sessionEvents = broadcastCall?.[session.sessionId ?? ''] + expect(sessionEvents === undefined || sessionEvents.length === 0).toBe(true) + }) + + it('should handle NotificationEventType.UpdateNotification', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + type: NotificationEventType.UpdateNotification, + account: accountUuid + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle NotificationEventType.RemoveNotifications', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + type: NotificationEventType.RemoveNotifications, + account: accountUuid + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle NotificationEventType.CreateNotificationContext', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + type: NotificationEventType.CreateNotificationContext, + account: accountUuid + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle NotificationEventType.UpdateNotificationContext', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + type: NotificationEventType.UpdateNotificationContext, + account: accountUuid + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle NotificationEventType.RemoveNotificationContext', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + type: NotificationEventType.RemoveNotificationContext, + account: accountUuid + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should broadcast NotificationEventType.AddCollaborators to all sessions', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + type: NotificationEventType.AddCollaborators + } as any + + middleware.handleBroadcast(session, [event]) + + expect(mockCallbacks.broadcast).toHaveBeenCalled() + const broadcastCall = mockCallbacks.broadcast.mock.calls[0][1] + expect(broadcastCall[session.sessionId ?? '']).toEqual([event]) + }) + + it('should broadcast NotificationEventType.RemoveCollaborators to all sessions', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.RemoveCollaborators + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle LabelEventType.CreateLabel for matching account', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: LabelEventType.CreateLabel, + account: accountUuid + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should handle LabelEventType.RemoveLabel for matching account', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: LabelEventType.RemoveLabel, + account: accountUuid + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should broadcast CardEventType.UpdateCardType to all sessions', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: CardEventType.UpdateCardType + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should broadcast CardEventType.RemoveCard to all sessions', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: CardEventType.RemoveCard + } as any + + middleware.handleBroadcast(session, [event]) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + }) + + it('should not broadcast PeerEventType.CreatePeer', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: PeerEventType.CreatePeer + } as any + + middleware.handleBroadcast(session, [event]) + + // Enqueue is still called + expect(mockCallbacks.enqueue).toHaveBeenCalled() + // No sessions should receive this event (either undefined or empty array) + const broadcastCall = mockCallbacks.broadcast.mock.calls[0]?.[1] + const sessionEvents = broadcastCall?.[session.sessionId ?? ''] + expect(sessionEvents === undefined || sessionEvents.length === 0).toBe(true) + }) + + it('should not broadcast PeerEventType.RemovePeer', async () => { + await middleware.event(session, createSessionEvent(), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: PeerEventType.RemovePeer + } as any + + middleware.handleBroadcast(session, [event]) + + // Enqueue is still called + expect(mockCallbacks.enqueue).toHaveBeenCalled() + // No sessions should receive this event (either undefined or empty array) + const broadcastCall = mockCallbacks.broadcast.mock.calls[0]?.[1] + const sessionEvents = broadcastCall?.[session.sessionId ?? ''] + expect(sessionEvents === undefined || sessionEvents.length === 0).toBe(true) + }) + + it('should handle broadcast errors gracefully', async () => { + mockCallbacks.broadcast.mockImplementation(() => { + throw new Error('Broadcast error') + }) + + await middleware.event(session, createSessionEvent(cardId), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + expect(() => { + middleware.handleBroadcast(session, [event]) + }).not.toThrow() + + expect(mockMeasureCtx.error).toHaveBeenCalledWith( + 'Failed to broadcast event', + expect.objectContaining({ error: expect.any(Error) }) + ) + }) + + it('should handle enqueue errors gracefully', async () => { + mockCallbacks.enqueue.mockImplementation(() => { + throw new Error('Enqueue error') + }) + + await middleware.event(session, createSessionEvent(cardId), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown + } + + expect(() => { + middleware.handleBroadcast(session, [event]) + }).not.toThrow() + + expect(mockMeasureCtx.error).toHaveBeenCalledWith( + 'Failed to broadcast event', + expect.objectContaining({ error: expect.any(Error) }) + ) + }) + + it('should pass context data to broadcast', async () => { + await middleware.event(session, createSessionEvent(cardId), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown + } + + middleware.handleBroadcast(session, [event]) + + expect(mockMeasureCtx.newChild).toHaveBeenCalledWith('enqueue', {}) + expect(mockCallbacks.broadcast).toHaveBeenCalled() + expect(mockCallbacks.enqueue).toHaveBeenCalled() + }) + }) + + describe('close and closeSession', () => { + it('should handle close', async () => { + await middleware.event(session, createSessionEvent(cardId), false) + await middleware.event(session2, createSessionEvent(cardId), false) + + middleware.subscribeCard(session, cardId, 'sub-123') + middleware.subscribeCard(session2, cardId, 'sub-456') + + expect(() => { + middleware.close() + }).not.toThrow() + }) + + it('should clear all sessions on close', async () => { + await middleware.event(session, createSessionEvent(cardId), false) + await middleware.event(session2, createSessionEvent(cardId), false) + + middleware.subscribeCard(session, cardId, 'sub-123') + middleware.subscribeCard(session2, cardId, 'sub-456') + + middleware.close() + + // After close, broadcast should not have any sessions + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown + } + + middleware.handleBroadcast(session, [event]) + + // Enqueue is still called but broadcast shouldn't be (no sessions) + expect(mockCallbacks.enqueue).toHaveBeenCalled() + // broadcast should not be called since there are no sessions + const broadcastCalls = mockCallbacks.broadcast.mock.calls + if (broadcastCalls.length > 0) { + const lastBroadcastCall = broadcastCalls[broadcastCalls.length - 1][1] + expect(Object.keys(lastBroadcastCall)).toHaveLength(0) + } + }) + + it('should handle closeSession', async () => { + await middleware.event(session, createSessionEvent(cardId), false) + middleware.subscribeCard(session, cardId, 'sub-123') + + expect(() => { + middleware.closeSession('session-123') + }).not.toThrow() + }) + + it('should remove specific session on closeSession', async () => { + await middleware.event(session, createSessionEvent(cardId), false) + await middleware.event(session2, createSessionEvent(cardId), false) + + middleware.subscribeCard(session, cardId, 'sub-123') + middleware.subscribeCard(session2, cardId, 'sub-456') + + middleware.closeSession('session-123') + + // After closeSession, only session2 should receive broadcasts + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + messageType: MessageType.Text, + content: 'Test' as Markdown + } + + middleware.handleBroadcast(session, [event]) + + expect(mockCallbacks.broadcast).toHaveBeenCalled() + const broadcastCall = mockCallbacks.broadcast.mock.calls[0][1] + expect(broadcastCall[session.sessionId ?? '']).toBeUndefined() + expect(broadcastCall[session2.sessionId ?? '']).toEqual([event]) + }) + + it('should handle closeSession for non-existent session', () => { + expect(() => { + middleware.closeSession('non-existent-session') + }).not.toThrow() + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/date.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/date.test.ts new file mode 100644 index 0000000000..252ac632fc --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/middleware/date.test.ts @@ -0,0 +1,249 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MeasureContext, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' +import { Event, MessageEventType, SessionData } from '@hcengineering/communication-sdk-types' +import { + AccountUuid, + CardID, + CardType, + Markdown, + MessageID, + MessageType, + SocialID +} from '@hcengineering/communication-types' + +import { DateMiddleware } from '../../middleware/date' +import { Enriched, Middleware, MiddlewareContext } from '../../types' +import { LowLevelClient } from '../../client' + +describe('DateMiddleware', () => { + let mockContext: MiddlewareContext + let mockClient: jest.Mocked + let mockMeasureCtx: jest.Mocked + let mockNext: jest.Mocked + let session: SessionData + let middleware: DateMiddleware + + const workspace = 'test-workspace' as WorkspaceUuid + const accountUuid = 'account-123' as AccountUuid + const socialId = 'social-123' as SocialID + + const basicEvent = { + _id: 'event-123', + _eventExtra: {}, + socialId, + date: new Date() + } as const + + beforeEach(() => { + jest.clearAllMocks() + + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis() + } as any as jest.Mocked + + mockClient = { + db: {}, + blob: {}, + findPersonUuid: jest.fn() + } as any as jest.Mocked + + mockNext = { + event: jest.fn().mockResolvedValue({}), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + handleBroadcast: jest.fn(), + closeSession: jest.fn(), + close: jest.fn() + } as any as jest.Mocked + + mockContext = { + ctx: mockMeasureCtx, + client: mockClient, + workspace, + metadata: { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'secret', + messagesPerBlob: 100 + }, + cadsWithPeers: new Set() + } + + session = { + account: { + uuid: accountUuid, + socialIds: [socialId] + }, + sessionId: 'session-123', + asyncData: [] + } as any as SessionData + + middleware = new DateMiddleware(mockContext, mockNext) + }) + + describe('event', () => { + it('should set date when not provided and not derived', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text + } + + const beforeTime = new Date().getTime() + await middleware.event(session, event, false) + const afterTime = new Date().getTime() + + expect(event.date).toBeDefined() + expect(event.date).toBeInstanceOf(Date) + expect(event.date.getTime()).toBeGreaterThanOrEqual(beforeTime) + expect(event.date.getTime()).toBeLessThanOrEqual(afterTime) + expect(event._eventExtra).toBeDefined() + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should preserve date when derived is true and date is provided', async () => { + const customDate = new Date('2025-01-01T00:00:00Z') + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text, + socialId, + date: customDate + } + + await middleware.event(session, event, true) + + expect(event.date).toEqual(customDate) + expect(mockNext.event).toHaveBeenCalledWith(session, event, true) + }) + + it('should preserve date when user is system account', async () => { + const customDate = new Date('2025-01-01T00:00:00Z') + const systemSession: SessionData = { + ...session, + account: { + uuid: systemAccountUuid, + socialIds: [socialId] + } + } as any as SessionData + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text, + socialId, + date: customDate + } + + await middleware.event(systemSession, event, false) + + expect(event.date).toEqual(customDate) + expect(mockNext.event).toHaveBeenCalledWith(systemSession, event, false) + }) + + it('should set date even when null is provided for non-system account', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text, + socialId, + date: null as any + } + + await middleware.event(session, event, false) + + expect(event.date).toBeDefined() + expect(event.date).toBeInstanceOf(Date) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should initialize _eventExtra if not present', async () => { + const event: Enriched = { + ...basicEvent, + _eventExtra: undefined as any, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text, + socialId + } + + await middleware.event(session, event, false) + + expect(event._eventExtra).toBeDefined() + expect(event._eventExtra).toEqual({}) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should preserve existing _eventExtra', async () => { + const existingExtra = { someData: 'value' } + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text, + socialId, + _eventExtra: existingExtra + } + + await middleware.event(session, event, false) + + expect(event._eventExtra).toEqual(existingExtra) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should handle events without date property', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.RemovePatch, + cardId: 'card-123' as CardID, + messageId: 'msg-123' as MessageID, + socialId, + date: undefined as any + } + + await middleware.event(session, event, false) + + expect(event.date).toBeDefined() + expect(event.date).toBeInstanceOf(Date) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/id.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/id.test.ts new file mode 100644 index 0000000000..8f88e8b13c --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/middleware/id.test.ts @@ -0,0 +1,236 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' +import { Event, MessageEventType, ReactionPatchEvent, SessionData } from '@hcengineering/communication-sdk-types' +import { + AccountUuid, + CardID, + CardType, Emoji, + Markdown, + MessageID, + MessageType, + SocialID +} from '@hcengineering/communication-types' + +import { IdMiddleware } from '../../middleware/id' +import { Enriched, MiddlewareContext, Middleware } from '../../types' +import { LowLevelClient } from '../../client' + +describe('IdMiddleware', () => { + let mockContext: MiddlewareContext + let mockClient: jest.Mocked + let mockMeasureCtx: jest.Mocked + let mockNext: jest.Mocked + let session: SessionData + let middleware: IdMiddleware + + const workspace = 'test-workspace' as WorkspaceUuid + const accountUuid = 'account-123' as AccountUuid + const socialId = 'social-123' as SocialID + + const basicEvent = { + _id: 'event-123', + _eventExtra: {}, + socialId, + date: new Date() + } as const + + beforeEach(() => { + jest.clearAllMocks() + + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis() + } as any as jest.Mocked + + mockClient = { + db: {}, + blob: {} + } as any as jest.Mocked + + mockNext = { + event: jest.fn().mockResolvedValue({}), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + handleBroadcast: jest.fn(), + closeSession: jest.fn(), + close: jest.fn() + } as any as jest.Mocked + + mockContext = { + ctx: mockMeasureCtx, + client: mockClient, + workspace, + metadata: { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'secret', + messagesPerBlob: 100 + }, + cadsWithPeers: new Set() + } + + session = { + account: { + uuid: accountUuid, + socialIds: [socialId] + }, + sessionId: 'session-123', + asyncData: [] + } as any as SessionData + + middleware = new IdMiddleware(mockContext, mockNext) + }) + + describe('CreateMessage events', () => { + it('should generate messageId when not provided', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'Test message' as Markdown, + messageType: MessageType.Text + } + + await middleware.event(session, event, false) + + expect(event.messageId).toBeDefined() + expect(typeof event.messageId).toBe('string') + // @ts-expect-error check messageId + expect(event.messageId.length).toBeGreaterThan(0) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should generate unique messageIds for different events', async () => { + const event1: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'First message' as Markdown, + messageType: MessageType.Text + } + + const event2: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'Second message' as Markdown, + messageType: MessageType.Text + } + + await middleware.event(session, event1, false) + await middleware.event(session, event2, false) + + expect(event1.messageId).toBeDefined() + expect(event2.messageId).toBeDefined() + expect(event1.messageId).not.toBe(event2.messageId) + }) + + it('should preserve existing messageId', async () => { + const existingMessageId = 'existing-msg-id' as MessageID + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'Test message' as Markdown, + messageType: MessageType.Text, + messageId: existingMessageId + } + + await middleware.event(session, event, false) + + expect(event.messageId).toBe(existingMessageId) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should generate messageId for derived CreateMessage events', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text + } + + await middleware.event(session, event, true) + + expect(event.messageId).toBeDefined() + expect(typeof event.messageId).toBe('string') + expect(mockNext.event).toHaveBeenCalledWith(session, event, true) + }) + }) + + describe('Non-CreateMessage events', () => { + it('should not modify messageId for UpdatePatch events', async () => { + const messageId = 'msg-123' as MessageID + const event: Enriched = { + ...basicEvent, + type: MessageEventType.UpdatePatch, + cardId: 'card-123' as CardID, + messageId, + content: 'Updated' as Markdown + } + + await middleware.event(session, event, false) + + expect(event.messageId).toBe(messageId) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should not modify messageId for RemovePatch events', async () => { + const messageId = 'msg-123' as MessageID + const event: Enriched = { + ...basicEvent, + type: MessageEventType.RemovePatch, + cardId: 'card-123' as CardID, + messageId + } + + await middleware.event(session, event, false) + + expect(event.messageId).toBe(messageId) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should not modify messageId for ReactionPatch events', async () => { + const messageId = 'msg-123' as MessageID + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ReactionPatch, + cardId: 'card-123' as CardID, + messageId, + operation: { opcode: 'add', reaction: '👍' as Emoji } + } + + await middleware.event(session, event, false) + + expect(event.messageId).toBe(messageId) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/indentity.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/indentity.test.ts new file mode 100644 index 0000000000..81b843dfd6 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/middleware/indentity.test.ts @@ -0,0 +1,332 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MeasureContext, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' +import { Event, MessageEventType, SessionData } from '@hcengineering/communication-sdk-types' +import { + AccountUuid, + CardID, + CardType, + Emoji, + Markdown, + MessageID, + MessageType, + SocialID +} from '@hcengineering/communication-types' +import { IdentityMiddleware } from '../../middleware/indentity' +import { Enriched, Middleware, MiddlewareContext } from '../../types' +import { LowLevelClient } from '../../client' + +describe('IdentityMiddleware', () => { + let mockContext: MiddlewareContext + let mockClient: jest.Mocked + let mockMeasureCtx: jest.Mocked + let mockNext: any + let session: SessionData + let middleware: IdentityMiddleware + let findPersonUuidMock: jest.MockedFunction + + const workspace = 'test-workspace' as WorkspaceUuid + const accountUuid = 'account-123' as AccountUuid + const socialId = 'social-123' as SocialID + + const basicEvent = { + _id: 'event-123', + _eventExtra: {}, + socialId, + date: new Date() + } as const + + beforeEach(() => { + jest.clearAllMocks() + + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis() + } as any as jest.Mocked + + const findPersonUuidFn = jest.fn() + + mockClient = { + db: {}, + blob: {}, + findPersonUuid: findPersonUuidFn + } as any as jest.Mocked + + findPersonUuidMock = findPersonUuidFn + + mockNext = { + event: jest.fn().mockResolvedValue({}), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + handleBroadcast: jest.fn(), + closeSession: jest.fn(), + close: jest.fn() + } as any as jest.Mocked + + mockContext = { + ctx: mockMeasureCtx, + client: mockClient, + workspace, + metadata: { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'secret', + messagesPerBlob: 100 + }, + cadsWithPeers: new Set() + } + + session = { + account: { + uuid: accountUuid, + socialIds: [socialId] + }, + sessionId: 'session-123' + } as any as SessionData + + middleware = new IdentityMiddleware(mockContext, mockNext) + }) + + describe('event - personUuid enrichment', () => { + it('should set personUuid for ThreadPatch event', async () => { + const personUuid = 'person-123' + findPersonUuidMock.mockResolvedValue(personUuid as any) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ThreadPatch, + cardId: 'card-123' as CardID, + messageId: 'msg-123' as MessageID, + operation: { opcode: 'attach', threadId: 'thread-123' as CardID, threadType: 'threadType' as CardType }, + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + + expect(event.personUuid).toBe(personUuid) + expect(findPersonUuidMock).toHaveBeenCalledWith( + { ctx: mockMeasureCtx, account: session.account }, + socialId + ) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should set personUuid for ReactionPatch event', async () => { + const personUuid = 'person-456' + findPersonUuidMock.mockResolvedValue(personUuid as any) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ReactionPatch, + cardId: 'card-123' as CardID, + messageId: 'msg-123' as MessageID, + operation: { opcode: 'add', reaction: '👍' as Emoji } + } + + await middleware.event(session, event, false) + + expect(event.personUuid).toBe(personUuid) + expect(findPersonUuidMock).toHaveBeenCalledWith( + { ctx: mockMeasureCtx, account: session.account }, + socialId + ) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should not set personUuid for CreateMessage event', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text + } + + await middleware.event(session, event, false) + + expect((event as any).personUuid).toBeUndefined() + expect(findPersonUuidMock).not.toHaveBeenCalled() + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should handle personUuid as undefined when not found', async () => { + findPersonUuidMock.mockResolvedValue(undefined) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ThreadPatch, + cardId: 'card-123' as CardID, + messageId: 'msg-123' as MessageID, + operation: { opcode: 'update', threadId: 'thread-123' as CardID, update: { threadType: 'threadType' as CardType } } + } + + await middleware.event(session, event, false) + + expect(event.personUuid).toBeUndefined() + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should work with derived events', async () => { + const personUuid = 'person-789' + findPersonUuidMock.mockResolvedValue(personUuid as any) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ReactionPatch, + cardId: 'card-123' as CardID, + messageId: 'msg-123' as MessageID, + operation: { opcode: 'remove', reaction: '❤️' as Emoji } + } + + await middleware.event(session, event, true) + + expect(event.personUuid).toBe(personUuid) + expect(mockNext.event).toHaveBeenCalledWith(session, event, true) + }) + }) + + describe('findNotificationContexts - account enrichment', () => { + it('should enrich params with account for regular user', async () => { + const params = { cardId: 'card-123' as CardID } + await middleware.findNotificationContexts(session, params) + + expect(mockNext.findNotificationContexts).toHaveBeenCalledWith( + session, + { ...params, account: accountUuid }, + undefined + ) + }) + + it('should not enrich params for system account', async () => { + const systemSession: SessionData = { + ...session, + account: { uuid: systemAccountUuid, socialIds: [socialId] } + } as any as SessionData + + const params = { cardId: 'card-123' as CardID } + await middleware.findNotificationContexts(systemSession, params) + + expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(systemSession, params, undefined) + }) + + it('should overwrite existing account in params with session account', async () => { + const otherAccount = 'other-account' as AccountUuid + const params = { cardId: 'card-123' as CardID, account: otherAccount } + await middleware.findNotificationContexts(session, params) + + expect(mockNext.findNotificationContexts).toHaveBeenCalledWith( + session, + { ...params, account: accountUuid }, // Session account overwrites the other account + undefined + ) + }) + + it('should pass subscription to next middleware', async () => { + const params = { cardId: 'card-123' as CardID } + const subscription = 'sub-123' + await middleware.findNotificationContexts(session, params, subscription) + + expect(mockNext.findNotificationContexts).toHaveBeenCalledWith( + session, + { ...params, account: accountUuid }, + subscription + ) + }) + }) + + describe('findNotifications - account enrichment', () => { + it('should enrich params with account for regular user', async () => { + const params = { contextId: 'ctx-123' as any } + await middleware.findNotifications(session, params) + + expect(mockNext.findNotifications).toHaveBeenCalledWith( + session, + { ...params, account: accountUuid }, + undefined + ) + }) + + it('should not enrich params for system account', async () => { + const systemSession: SessionData = { + ...session, + account: { uuid: systemAccountUuid, socialIds: [socialId] } + } as any as SessionData + + const params = { contextId: 'ctx-123' as any } + await middleware.findNotifications(systemSession, params) + + expect(mockNext.findNotifications).toHaveBeenCalledWith(systemSession, params, undefined) + }) + + it('should overwrite existing account in params with session account', async () => { + const otherAccount = 'other-account' as AccountUuid + const params = { contextId: 'ctx-123' as any, account: otherAccount } + await middleware.findNotifications(session, params) + + expect(mockNext.findNotifications).toHaveBeenCalledWith( + session, + { ...params, account: accountUuid }, // Session account overwrites the other account + undefined + ) + }) + }) + + describe('findLabels - account enrichment', () => { + it('should enrich params with account for regular user', async () => { + const params = { cardId: 'card-123' as CardID } + await middleware.findLabels(session, params) + + expect(mockNext.findLabels).toHaveBeenCalledWith( + session, + { ...params, account: accountUuid }, + undefined + ) + }) + + it('should not enrich params for system account', async () => { + const systemSession: SessionData = { + ...session, + account: { uuid: systemAccountUuid, socialIds: [socialId] } + } as any as SessionData + + const params = { cardId: 'card-123' as CardID } + await middleware.findLabels(systemSession, params) + + expect(mockNext.findLabels).toHaveBeenCalledWith(systemSession, params, undefined) + }) + + it('should overwrite existing account in params with session account', async () => { + const otherAccount = 'other-account' as AccountUuid + const params = { cardId: 'card-123' as CardID, account: otherAccount } + await middleware.findLabels(session, params) + + expect(mockNext.findLabels).toHaveBeenCalledWith( + session, + { ...params, account: accountUuid }, // Session account overwrites the other account + undefined + ) + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/peer.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/peer.test.ts new file mode 100644 index 0000000000..e2723959ff --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/middleware/peer.test.ts @@ -0,0 +1,323 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' +import { + AttachmentPatchEvent, + CreateMessageEvent, + CreatePeerEvent, + Event, + MessageEventType, + NotificationEventType, + PeerEventType, RemovePatchEvent, + SessionData, UpdatePatchEvent +} from '@hcengineering/communication-sdk-types' +import { + AccountUuid, + CardID, + CardType, + Markdown, + MessageID, + MessageType, + SocialID +} from '@hcengineering/communication-types' +import { PeerMiddleware } from '../../middleware/peer' +import { Enriched, Middleware, MiddlewareContext } from '../../types' +import { LowLevelClient } from '../../client' + +describe('PeerMiddleware', () => { + let mockContext: MiddlewareContext + let mockClient: jest.Mocked + let mockMeasureCtx: jest.Mocked + let mockNext: any + let mockHead: any + let session: SessionData + let middleware: PeerMiddleware + + const workspace = 'test-workspace' as WorkspaceUuid + const accountUuid = 'account-123' as AccountUuid + const socialId = 'social-123' as SocialID + const cardId = 'card-123' as CardID + + const basicEvent = { + _id: 'event-123', + _eventExtra: {}, + socialId, + date: new Date() + } as const + + beforeEach(() => { + jest.clearAllMocks() + + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis() + } as any as jest.Mocked + + mockClient = { + db: {}, + blob: {} + } as any as jest.Mocked + + mockNext = { + event: jest.fn().mockResolvedValue({}), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + handleBroadcast: jest.fn(), + closeSession: jest.fn(), + close: jest.fn() + } as any as jest.Mocked + + mockHead = { + findPeers: jest.fn().mockResolvedValue([]), + event: jest.fn().mockResolvedValue({}), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + handleBroadcast: jest.fn(), + closeSession: jest.fn(), + close: jest.fn() + } as any as jest.Mocked + + mockContext = { + ctx: mockMeasureCtx, + client: mockClient, + workspace, + metadata: { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'secret', + messagesPerBlob: 100 + }, + cadsWithPeers: new Set(), + head: mockHead + } + + session = { + account: { + uuid: accountUuid, + socialIds: [socialId] + }, + sessionId: 'session-123' + } as any as SessionData + + middleware = new PeerMiddleware(mockContext, mockNext) + }) + + describe('CreatePeer events', () => { + it('should add cardId to cadsWithPeers on CreatePeer event', async () => { + const event: Enriched = { + ...basicEvent, + workspaceId: workspace, + kind: 'card', + type: PeerEventType.CreatePeer, + cardId, + value: '123' + } + + expect(mockContext.cadsWithPeers.has(cardId as any)).toBe(false) + + await middleware.event(session, event, false) + + expect(mockContext.cadsWithPeers.has(cardId as any)).toBe(true) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should work with derived CreatePeer events', async () => { + const event: Enriched = { + ...basicEvent, + workspaceId: workspace, + kind: 'card', + type: PeerEventType.CreatePeer, + cardId, + value: '123' + } + + await middleware.event(session, event, true) + + expect(mockContext.cadsWithPeers.has(cardId as any)).toBe(true) + expect(mockNext.event).toHaveBeenCalledWith(session, event, true) + }) + }) + + describe('Message events with peers', () => { + beforeEach(() => { + mockContext.cadsWithPeers.add(cardId as any) + }) + + it('should fetch and attach peers for CreateMessage event', async () => { + const peers = [ + { kind: 'card', members: [{ workspaceId: workspace, cardId: 'peer-card' }] } + ] + mockHead.findPeers.mockResolvedValue(peers) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text + } + + await middleware.event(session, event, false) + + expect(mockHead.findPeers).toHaveBeenCalledWith(session, { + workspaceId: workspace, + cardId + }) + expect(event._eventExtra?.peers).toEqual(peers) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should fetch and attach peers for UpdatePatch event', async () => { + const peers = [{ kind: 'card', members: [] }] + mockHead.findPeers.mockResolvedValue(peers) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.UpdatePatch, + cardId, + messageId: 'msg-123' as MessageID, + content: 'Updated' as Markdown + } + + await middleware.event(session, event, false) + + expect(mockHead.findPeers).toHaveBeenCalledWith(session, { + workspaceId: workspace, + cardId + }) + expect(event._eventExtra?.peers).toEqual(peers) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should fetch and attach peers for RemovePatch event', async () => { + const peers = [{ kind: 'card', members: [] }] + mockHead.findPeers.mockResolvedValue(peers) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.RemovePatch, + cardId, + messageId: 'msg-123' as MessageID + } + + await middleware.event(session, event, false) + + expect(event._eventExtra?.peers).toEqual(peers) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should fetch and attach peers for AttachmentPatch event', async () => { + const peers = [{ kind: 'card', members: [] }] + mockHead.findPeers.mockResolvedValue(peers) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.AttachmentPatch, + cardId, + messageId: 'msg-123' as MessageID, + operations: [] + } + + await middleware.event(session, event, false) + + expect(event._eventExtra?.peers).toEqual(peers) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should initialize _eventExtra if not present', async () => { + const peers = [{ kind: 'card', members: [] }] + mockHead.findPeers.mockResolvedValue(peers) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text + } + + await middleware.event(session, event, false) + + expect(event._eventExtra).toBeDefined() + expect(event._eventExtra?.peers).toEqual(peers) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should handle errors when fetching peers', async () => { + mockHead.findPeers.mockRejectedValue(new Error('Fetch error')) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text + } + + await expect(middleware.event(session, event, false)).rejects.toThrow('Fetch error') + }) + }) + + describe('Message events without peers', () => { + it('should not fetch peers when cardId not in cadsWithPeers', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId: 'card-456' as CardID, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text + } + + await middleware.event(session, event, false) + + expect(mockHead.findPeers).not.toHaveBeenCalled() + expect(event._eventExtra?.peers).toEqual([{ kind: 'card', members: [] }]) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + }) + + describe('Non-message events', () => { + it('should not fetch peers for notification events', async () => { + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.CreateNotification, + account: accountUuid + } as any as Enriched + + await middleware.event(session, event, false) + + expect(mockHead.findPeers).not.toHaveBeenCalled() + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/permissions.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/permissions.test.ts new file mode 100644 index 0000000000..6adb9cbd32 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/middleware/permissions.test.ts @@ -0,0 +1,759 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { AccountRole, MeasureContext, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' +import { + Event, + MessageEventType, + NotificationEventType, + PeerEventType, + SessionData +} from '@hcengineering/communication-sdk-types' +import { + AccountUuid, + CardID, + CardType, ContextID, Emoji, + Markdown, + MessageID, + MessageType, NotificationID, + SocialID +} from '@hcengineering/communication-types' + +import { PermissionsMiddleware } from '../../middleware/permissions' +import { Enriched, Middleware, MiddlewareContext } from '../../types' +import { LowLevelClient } from '../../client' + +describe('PermissionsMiddleware', () => { + let mockContext: MiddlewareContext + let mockClient: jest.Mocked + let mockMeasureCtx: jest.Mocked + let mockNext: any + let session: SessionData + let middleware: PermissionsMiddleware + let getMessageMetaMock: jest.MockedFunction + + const workspace = 'test-workspace' as WorkspaceUuid + const accountUuid = 'account-123' as AccountUuid + const socialId = 'social-123' as SocialID + const cardId = 'card-123' as CardID + + const basicEvent = { + _id: 'event-123', + _eventExtra: {}, + socialId, + date: new Date() + } as const + + beforeEach(() => { + jest.clearAllMocks() + + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis() + } as any as jest.Mocked + + const getMessageMetaFn = jest.fn() + + mockClient = { + db: {}, + blob: {}, + getMessageMeta: getMessageMetaFn + } as any as jest.Mocked + + getMessageMetaMock = getMessageMetaFn + + mockNext = { + event: jest.fn().mockResolvedValue({}), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + handleBroadcast: jest.fn(), + closeSession: jest.fn(), + close: jest.fn() + } as any as jest.Mocked + + mockContext = { + ctx: mockMeasureCtx, + client: mockClient, + workspace, + metadata: { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'secret', + messagesPerBlob: 100 + }, + cadsWithPeers: new Set() + } + + session = { + account: { + uuid: accountUuid, + socialIds: [socialId], + role: AccountRole.User + }, + sessionId: 'session-123' + } as any as SessionData + + middleware = new PermissionsMiddleware(mockContext, mockNext) + }) + + describe('Derived events', () => { + it('should skip permission checks for derived events', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text, + socialId: 'other-social' as SocialID + } + + await middleware.event(session, event, true) + + expect(mockNext.event).toHaveBeenCalledWith(session, event, true) + }) + + it('should allow any operation for derived events', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.UpdatePatch, + cardId, + messageId: 'msg-123' as MessageID, + content: 'Updated' as Markdown, + socialId: 'other-social' as SocialID + } + + await middleware.event(session, event, true) + + expect(getMessageMetaMock).not.toHaveBeenCalled() + expect(mockNext.event).toHaveBeenCalledWith(session, event, true) + }) + }) + + describe('System account', () => { + it('should allow all operations for system account', async () => { + const systemSession: SessionData = { + ...session, + account: { + uuid: systemAccountUuid, + socialIds: [socialId], + role: AccountRole.User + } + } as any as SessionData + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text, + socialId: 'any-social' as SocialID + } + + await middleware.event(systemSession, event, false) + + expect(mockNext.event).toHaveBeenCalledWith(systemSession, event, false) + }) + + it('should allow system account to use any socialId', async () => { + const systemSession: SessionData = { + ...session, + account: { + uuid: systemAccountUuid, + socialIds: [socialId], + role: AccountRole.User + } + } as any as SessionData + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ReactionPatch, + cardId, + messageId: 'msg-123' as MessageID, + operation: { opcode: 'add', reaction: '👍' as Emoji }, + socialId: 'different-social' as SocialID + } + + await middleware.event(systemSession, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('CreateMessage events', () => { + it('should allow CreateMessage with correct socialId', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should reject CreateMessage with incorrect socialId', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text, + socialId: 'other-social' as SocialID + } + + await expect(middleware.event(session, event, false)).rejects.toThrow() + }) + + it('should force noNotify to false for non-system accounts', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text, + options: { noNotify: true } + } + + await middleware.event(session, event, false) + + expect(event.options?.noNotify).toBe(false) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should allow noNotify for system account', async () => { + const systemSession: SessionData = { + ...session, + account: { + uuid: systemAccountUuid, + socialIds: [socialId], + role: AccountRole.User + } + } as any as SessionData + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text, + options: { noNotify: true } + } + + await middleware.event(systemSession, event, false) + + expect(event.options?.noNotify).toBe(true) + expect(mockNext.event).toHaveBeenCalledWith(systemSession, event, false) + }) + }) + + describe('UpdatePatch events', () => { + it('should allow UpdatePatch when user is message author', async () => { + const messageId = 'msg-123' as MessageID + getMessageMetaMock.mockResolvedValue({ + creator: socialId, + cardId, + id: messageId + } as any) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.UpdatePatch, + cardId, + messageId, + content: 'Updated' as Markdown + } + + await middleware.event(session, event, false) + + expect(getMessageMetaMock).toHaveBeenCalledWith(cardId, messageId) + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should reject UpdatePatch when user is not message author', async () => { + const messageId = 'msg-123' as MessageID + getMessageMetaMock.mockResolvedValue({ + creator: 'other-social' as SocialID, + cardId, + id: messageId + } as any) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.UpdatePatch, + cardId, + messageId, + content: 'Updated' as Markdown + } + + await expect(middleware.event(session, event, false)).rejects.toThrow('message author is not allowed') + }) + + it('should reject UpdatePatch when message not found', async () => { + getMessageMetaMock.mockResolvedValue(undefined) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.UpdatePatch, + cardId, + messageId: 'msg-123' as MessageID, + content: 'Updated' as Markdown + } + + await expect(middleware.event(session, event, false)).rejects.toThrow('message not found') + }) + + it('should reject UpdatePatch with incorrect socialId', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.UpdatePatch, + cardId, + messageId: 'msg-123' as MessageID, + content: 'Updated' as Markdown, + socialId: 'other-social' as SocialID + } + + await expect(middleware.event(session, event, false)).rejects.toThrow() + }) + }) + + describe('RemovePatch events', () => { + it('should allow RemovePatch when user is message author', async () => { + const messageId = 'msg-123' as MessageID + getMessageMetaMock.mockResolvedValue({ + creator: socialId, + cardId, + id: messageId + } as any) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.RemovePatch, + cardId, + messageId + } + + await middleware.event(session, event, false) + + expect(getMessageMetaMock).toHaveBeenCalledWith(cardId, messageId) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject RemovePatch when user is not message author', async () => { + const messageId = 'msg-123' as MessageID + getMessageMetaMock.mockResolvedValue({ + creator: 'other-social' as SocialID, + cardId, + id: messageId + } as any) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.RemovePatch, + cardId, + messageId + } + + await expect(middleware.event(session, event, false)).rejects.toThrow('message author is not allowed') + }) + }) + + describe('AttachmentPatch and BlobPatch events', () => { + it('should allow AttachmentPatch when user is message author', async () => { + const messageId = 'msg-123' as MessageID + getMessageMetaMock.mockResolvedValue({ + creator: socialId, + cardId, + id: messageId + } as any) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.AttachmentPatch, + cardId, + messageId, + operations: [] + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should allow BlobPatch when user is message author', async () => { + const messageId = 'msg-123' as MessageID + getMessageMetaMock.mockResolvedValue({ + creator: socialId, + cardId, + id: messageId + } as any) + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.BlobPatch, + cardId, + messageId, + operations: [] + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('ReactionPatch and ThreadPatch events', () => { + it('should allow ReactionPatch with correct socialId', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ReactionPatch, + cardId, + messageId: 'msg-123' as MessageID, + operation: { opcode: 'add', reaction: '👍' as Emoji } + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should reject ReactionPatch with incorrect socialId', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ReactionPatch, + cardId, + messageId: 'msg-123' as MessageID, + operation: { opcode: 'add', reaction: '👍' as Emoji }, + socialId: 'other-social' as SocialID + } + + await expect(middleware.event(session, event, false)).rejects.toThrow() + }) + + it('should allow ThreadPatch with correct socialId', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ThreadPatch, + cardId, + messageId: 'msg-123' as MessageID, + operation: { opcode: 'attach', threadId: 'thread-123' as CardID, threadType: 'threadType' as CardType } + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject ThreadPatch with incorrect socialId', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.ThreadPatch, + cardId, + messageId: 'msg-123' as MessageID, + operation: { opcode: 'attach', threadId: 'thread-123' as CardID, threadType: 'threadType' as CardType }, + socialId: 'other-social' as SocialID + } + + await expect(middleware.event(session, event, false)).rejects.toThrow() + }) + }) + + describe('Notification events', () => { + it('should allow UpdateNotificationContext for own account', async () => { + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.UpdateNotificationContext, + contextId: 'ctx-123' as ContextID, + account: accountUuid, + updates: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject UpdateNotificationContext for other account', async () => { + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.UpdateNotificationContext, + contextId: 'ctx-123' as ContextID, + account: 'other-account' as AccountUuid, + updates: {} + } + + await expect(middleware.event(session, event, false)).rejects.toThrow() + }) + + it('should allow RemoveNotifications for own account', async () => { + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.RemoveNotifications, + contextId: 'ctx-123' as ContextID, + account: accountUuid, + ids: ['notif-1' as NotificationID] + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject RemoveNotifications for other account', async () => { + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.RemoveNotifications, + contextId: 'ctx-123' as ContextID, + account: 'other-account' as AccountUuid, + ids: ['notif-1' as NotificationID] + } + + await expect(middleware.event(session, event, false)).rejects.toThrow() + }) + + it('should allow UpdateNotification for own account', async () => { + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.UpdateNotification, + contextId: 'ctx-123' as ContextID, + account: accountUuid, + updates: { read: true }, + query: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should allow RemoveNotificationContext for own account', async () => { + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.RemoveNotificationContext, + contextId: 'ctx-123' as ContextID, + account: accountUuid + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('System-only operations', () => { + it('should reject TranslateMessage for non-system account', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.TranslateMessage, + cardId, + content: '', + messageId: 'msg-123' as MessageID, + language: 'en' + } + + await expect(middleware.event(session, event, false)).rejects.toThrow() + }) + + it('should allow TranslateMessage for system account', async () => { + const systemSession: SessionData = { + ...session, + account: { + uuid: systemAccountUuid, + socialIds: [socialId], + role: AccountRole.User + } + } as any as SessionData + + const event: Enriched = { + ...basicEvent, + type: MessageEventType.TranslateMessage, + cardId, + messageId: 'msg-123' as MessageID, + language: 'en', + content: '' + } + + await middleware.event(systemSession, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject CreatePeer for non-system account', async () => { + const event: Enriched = { + ...basicEvent, + type: PeerEventType.CreatePeer, + workspaceId: workspace, + cardId, + value: '123', + kind: 'card' + } + + await expect(middleware.event(session, event, false)).rejects.toThrow() + }) + + it('should allow CreatePeer for system account', async () => { + const systemSession: SessionData = { + ...session, + account: { + uuid: systemAccountUuid, + socialIds: [socialId], + role: AccountRole.User + } + } as any as SessionData + + const event: Enriched = { + ...basicEvent, + type: PeerEventType.CreatePeer, + workspaceId: workspace, + cardId, + value: '123', + kind: 'card' + } + + await middleware.event(systemSession, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject RemovePeer for non-system account', async () => { + const event: Enriched = { + ...basicEvent, + type: PeerEventType.RemovePeer, + workspaceId: workspace, + cardId, + kind: 'card', + value: '123' + } + + await expect(middleware.event(session, event, false)).rejects.toThrow() + }) + + it('should allow RemovePeer for system account', async () => { + const systemSession: SessionData = { + ...session, + account: { + uuid: systemAccountUuid, + socialIds: [socialId], + role: AccountRole.User + } + } as any as SessionData + + const event: Enriched = { + ...basicEvent, + workspaceId: workspace, + type: PeerEventType.RemovePeer, + cardId, + kind: 'card', + value: '123' + } + + await middleware.event(systemSession, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('Collaborators events', () => { + it('should allow AddCollaborators with correct socialId', async () => { + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.AddCollaborators, + cardId, + cardType: 'task' as CardType, + collaborators: [accountUuid] + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject AddCollaborators with incorrect socialId', async () => { + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.AddCollaborators, + cardId, + cardType: 'task' as CardType, + collaborators: [accountUuid], + socialId: 'other-social' as SocialID + } + + await expect(middleware.event(session, event, false)).rejects.toThrow() + }) + + it('should allow RemoveCollaborators with correct socialId', async () => { + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.RemoveCollaborators, + cardId, + cardType: 'task' as CardType, + collaborators: [accountUuid] + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject RemoveCollaborators with incorrect socialId', async () => { + const event: Enriched = { + ...basicEvent, + type: NotificationEventType.RemoveCollaborators, + cardId, + cardType: 'task' as CardType, + collaborators: [accountUuid], + socialId: 'other-social' as SocialID + } + + await expect(middleware.event(session, event, false)).rejects.toThrow() + }) + }) + + describe('Middleware chaining', () => { + it('should call next middleware and return its result', async () => { + const event: Enriched = { + ...basicEvent, + type: MessageEventType.CreateMessage, + cardId, + cardType: 'task' as CardType, + content: 'Test' as Markdown, + messageType: MessageType.Text + } + + const expectedResult = { success: true, eventId: 'event-123' } + mockNext.event.mockResolvedValue(expectedResult) + + const result = await middleware.event(session, event, false) + + expect(result).toEqual(expectedResult) + expect(mockNext.event).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/storage.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/storage.test.ts new file mode 100644 index 0000000000..9d48a40ab0 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/middleware/storage.test.ts @@ -0,0 +1,1156 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' +import { MessageEventType, NotificationEventType, SessionData, LabelEventType, CardEventType, PeerEventType } from '@hcengineering/communication-sdk-types' +import { AccountUuid, CardType, Markdown, SocialID } from '@hcengineering/communication-types' +import { StorageMiddleware } from '../../middleware/storage' +import { Enriched, Middleware, MiddlewareContext } from '../../types' +import { LowLevelClient } from '../../client' + +describe('StorageMiddleware', () => { + let mockContext: MiddlewareContext + let mockClient: { + db: Record + blob: Record + getMessageMeta: jest.Mock + findPersonUuid: jest.Mock + removeMessageMeta: jest.Mock + } + let mockMeasureCtx: jest.Mocked + let mockNext: jest.Mocked + let session: SessionData + let middleware: StorageMiddleware + + const workspace = 'test-workspace' as WorkspaceUuid + const accountUuid = 'account-123' as AccountUuid + const socialId = 'social-123' as SocialID + + beforeEach(() => { + jest.clearAllMocks() + + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis() + } as any as jest.Mocked + + mockClient = { + db: { + findMessagesMeta: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + findThreadMeta: jest.fn().mockResolvedValue([]), + createMessageMeta: jest.fn().mockResolvedValue(true), + removeMessageMeta: jest.fn().mockResolvedValue(undefined), + addCollaborators: jest.fn().mockResolvedValue([]), + removeCollaborators: jest.fn().mockResolvedValue(undefined), + createNotification: jest.fn().mockResolvedValue('notif-123'), + updateNotification: jest.fn().mockResolvedValue(1), + removeNotifications: jest.fn().mockResolvedValue([]), + createNotificationContext: jest.fn().mockResolvedValue('ctx-123'), + removeContext: jest.fn().mockResolvedValue('ctx-123'), + updateContext: jest.fn().mockResolvedValue(undefined), + createLabel: jest.fn().mockResolvedValue(undefined), + removeLabels: jest.fn().mockResolvedValue(undefined), + createPeer: jest.fn().mockResolvedValue(undefined), + removePeer: jest.fn().mockResolvedValue(undefined), + attachThreadMeta: jest.fn().mockResolvedValue(undefined), + close: jest.fn() + }, + blob: { + findMessagesGroups: jest.fn().mockResolvedValue([]), + getMessageGroupByDate: jest.fn().mockResolvedValue({ blobId: 'blob-123' }), + insertMessage: jest.fn().mockResolvedValue(undefined), + updateMessage: jest.fn().mockResolvedValue(undefined), + removeMessage: jest.fn().mockResolvedValue(undefined), + addReaction: jest.fn().mockResolvedValue(undefined), + removeReaction: jest.fn().mockResolvedValue(undefined), + addAttachments: jest.fn().mockResolvedValue(undefined), + removeAttachments: jest.fn().mockResolvedValue(undefined), + setAttachments: jest.fn().mockResolvedValue(undefined), + updateAttachments: jest.fn().mockResolvedValue(undefined), + attachThread: jest.fn().mockResolvedValue(undefined), + detachThread: jest.fn().mockResolvedValue(undefined), + addBlobs: jest.fn().mockResolvedValue(undefined), + removeBlobs: jest.fn().mockResolvedValue(undefined), + updateBlobs: jest.fn().mockResolvedValue(undefined), + setBlobs: jest.fn().mockResolvedValue(undefined), + updateThread: jest.fn().mockResolvedValue(undefined), + addThreadReply: jest.fn().mockResolvedValue(undefined), + removeThreadReply: jest.fn().mockResolvedValue(undefined) + }, + getMessageMeta: jest.fn().mockResolvedValue({ blobId: 'blob-123', messageId: 'msg-123' }), + findPersonUuid: jest.fn().mockResolvedValue('person-123'), + removeMessageMeta: jest.fn().mockResolvedValue(undefined) + } + + mockNext = { + event: jest.fn().mockResolvedValue({}), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + handleBroadcast: jest.fn(), + closeSession: jest.fn(), + close: jest.fn() + } as any as jest.Mocked + + mockContext = { + ctx: mockMeasureCtx, + client: mockClient as any as LowLevelClient, + workspace, + metadata: { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'secret', + messagesPerBlob: 100 + }, + cadsWithPeers: new Set() + } + + session = { + account: { + uuid: accountUuid, + socialIds: [socialId] + }, + sessionId: 'session-123' + } as any as SessionData + + middleware = new StorageMiddleware(mockContext, mockNext) + }) + + describe('findMessagesMeta', () => { + it('should query database for messages meta', async () => { + const params = { cardId: 'card-123' } + const expectedResult = [{ messageId: 'msg-1', blobId: 'blob-1' }] + mockClient.db.findMessagesMeta.mockResolvedValue(expectedResult as any) + + const result = await middleware.findMessagesMeta(session, params as any) + + expect(mockClient.db.findMessagesMeta).toHaveBeenCalledWith(params) + expect(result).toEqual(expectedResult) + }) + }) + + describe('findMessagesGroups', () => { + it('should query blob storage for messages groups', async () => { + const params = { cardId: 'card-123' } + const expectedResult = [{ blobId: 'blob-1', from: new Date(), to: new Date() }] + mockClient.blob.findMessagesGroups.mockResolvedValue(expectedResult as any) + + const result = await middleware.findMessagesGroups(session, params as any) + + expect(mockClient.blob.findMessagesGroups).toHaveBeenCalledWith(params) + expect(result).toEqual(expectedResult) + }) + + it('should use message meta blobId when id is provided', async () => { + const params = { cardId: 'card-123', id: 'msg-123' } + mockClient.getMessageMeta.mockResolvedValue({ blobId: 'blob-456', messageId: 'msg-123' } as any) + + await middleware.findMessagesGroups(session, params as any) + + expect(mockClient.getMessageMeta).toHaveBeenCalledWith('card-123', 'msg-123') + expect(mockClient.blob.findMessagesGroups).toHaveBeenCalledWith({ + ...params, + blobId: 'blob-456' + }) + }) + + it('should return empty array if message meta not found', async () => { + const params = { cardId: 'card-123', id: 'msg-123' } + mockClient.getMessageMeta.mockResolvedValue(undefined) + + const result = await middleware.findMessagesGroups(session, params as any) + + expect(result).toEqual([]) + }) + }) + + describe('findNotificationContexts', () => { + it('should query database for notification contexts', async () => { + const params = { cardId: 'card-123', account: accountUuid } + const expectedResult = [{ id: 'ctx-1', cardId: 'card-123' }] + mockClient.db.findNotificationContexts.mockResolvedValue(expectedResult as any) + + const result = await middleware.findNotificationContexts(session, params as any) + + expect(mockClient.db.findNotificationContexts).toHaveBeenCalledWith(params + ) + expect(result).toEqual(expectedResult) + }) + }) + + describe('findNotifications', () => { + it('should query database for notifications', async () => { + const params = { contextId: 'ctx-123' } + const expectedResult = [{ id: 'notif-1', contextId: 'ctx-123' }] + mockClient.db.findNotifications.mockResolvedValue(expectedResult as any) + + const result = await middleware.findNotifications(session, params as any) + + expect(mockClient.db.findNotifications).toHaveBeenCalledWith(params) + expect(result).toEqual(expectedResult) + }) + }) + + describe('findLabels', () => { + it('should query database for labels', async () => { + const params = { cardId: 'card-123' } + const expectedResult = [{ labelId: 'label-1', cardId: 'card-123' }] + mockClient.db.findLabels.mockResolvedValue(expectedResult as any) + + const result = await middleware.findLabels(session, params as any) + + expect(mockClient.db.findLabels).toHaveBeenCalledWith(params) + expect(result).toEqual(expectedResult) + }) + }) + + describe('findCollaborators', () => { + it('should query database for collaborators', async () => { + const params = { cardId: 'card-123' } + const expectedResult = [{ account: accountUuid, cardId: 'card-123' }] + mockClient.db.findCollaborators.mockResolvedValue(expectedResult as any) + + const result = await middleware.findCollaborators(session, params as any) + + expect(mockClient.db.findCollaborators).toHaveBeenCalledWith(params) + expect(result).toEqual(expectedResult) + }) + }) + + describe('findPeers', () => { + it('should query database for peers', async () => { + const params = { workspaceId: workspace, cardId: 'card-123' } + const expectedResult = [{ workspaceId: workspace, cardId: 'card-123' }] + mockClient.db.findPeers.mockResolvedValue(expectedResult as any) + + const result = await middleware.findPeers(session, params as any) + + expect(mockClient.db.findPeers).toHaveBeenCalledWith(params) + expect(result).toEqual(expectedResult) + }) + }) + + describe('event - CreateMessage', () => { + it('should create message in storage', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + socialId, + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test message' as Markdown, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.blob.getMessageGroupByDate).toHaveBeenCalledWith('card-123', event.date) + expect(mockClient.db.createMessageMeta).toHaveBeenCalledWith( + 'card-123', + 'msg-123', + socialId, + event.date, + 'blob-123' + ) + expect(mockClient.blob.insertMessage).toHaveBeenCalled() + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should skip propagate if message already exists', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + socialId, + date: new Date(), + _eventExtra: {} + } + + mockClient.db.createMessageMeta.mockResolvedValue(false) + + await middleware.event(session, event, false) + + expect(event.skipPropagate).toBe(true) + expect(mockNext.event).not.toHaveBeenCalled() + }) + + it('should throw error if messageId is missing', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + socialId, + date: new Date(), + _eventExtra: {} + } + + await expect(middleware.event(session, event, false)).rejects.toThrow('Message id is required') + }) + + it('should throw error if message group not found', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + socialId, + date: new Date(), + _eventExtra: {} + } + + mockClient.blob.getMessageGroupByDate.mockResolvedValue(null) + + await expect(middleware.event(session, event, false)).rejects.toThrow('Cannot create message') + }) + }) + + describe('event - UpdatePatch', () => { + it('should update message in storage', async () => { + const event: Enriched = { + type: MessageEventType.UpdatePatch, + cardId: 'card-123', + messageId: 'msg-123', + content: 'Updated content' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.getMessageMeta).toHaveBeenCalledWith('card-123', 'msg-123') + expect(mockClient.blob.updateMessage).toHaveBeenCalledWith( + 'card-123', + 'blob-123', + 'msg-123', + { + content: event.content, + extra: event.extra, + language: event.language + }, + event.date + ) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should skip propagate if message not found', async () => { + const event: Enriched = { + type: MessageEventType.UpdatePatch, + cardId: 'card-123', + messageId: 'msg-123', + content: 'Updated' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + mockClient.getMessageMeta.mockResolvedValue(undefined) + + await middleware.event(session, event, false) + + expect(event.skipPropagate).toBe(true) + expect(mockNext.event).not.toHaveBeenCalled() + }) + }) + + describe('event - RemovePatch', () => { + it('should remove message from storage', async () => { + const event: Enriched = { + type: MessageEventType.RemovePatch, + cardId: 'card-123', + messageId: 'msg-123', + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.getMessageMeta).toHaveBeenCalledWith('card-123', 'msg-123') + expect(mockClient.blob.removeMessage).toHaveBeenCalledWith('card-123', 'blob-123', 'msg-123') + expect(mockClient.removeMessageMeta).toHaveBeenCalledWith('card-123', 'msg-123') + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should skip propagate if message not found', async () => { + const event: Enriched = { + type: MessageEventType.RemovePatch, + cardId: 'card-123', + messageId: 'msg-123', + socialId, + date: new Date(), + _eventExtra: {} + } + + mockClient.getMessageMeta.mockResolvedValue(undefined) + + await middleware.event(session, event, false) + + expect(event.skipPropagate).toBe(true) + expect(mockNext.event).not.toHaveBeenCalled() + }) + }) + + describe('event - ReactionPatch', () => { + it('should add reaction to message', async () => { + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId: 'card-123', + messageId: 'msg-123', + operation: { + opcode: 'add', + reaction: '👍' + }, + personUuid: 'person-123', + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.blob.addReaction).toHaveBeenCalledWith( + 'card-123', + 'blob-123', + 'msg-123', + '👍', + 'person-123', + event.date + ) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should remove reaction from message', async () => { + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId: 'card-123', + messageId: 'msg-123', + operation: { + opcode: 'remove', + reaction: '👍' + }, + personUuid: 'person-123', + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.blob.removeReaction).toHaveBeenCalledWith('card-123', 'blob-123', 'msg-123', '👍', 'person-123') + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should skip propagate if personUuid is missing', async () => { + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId: 'card-123', + messageId: 'msg-123', + operation: { + opcode: 'add', + reaction: '👍' + }, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(event.skipPropagate).toBe(true) + expect(mockNext.event).not.toHaveBeenCalled() + }) + }) + + describe('event - AttachmentPatch', () => { + it('should add attachments to message', async () => { + const event: Enriched = { + type: MessageEventType.AttachmentPatch, + cardId: 'card-123', + messageId: 'msg-123', + operations: [ + { + opcode: 'add', + attachments: [ + { + id: 'att-1', + mimeType: 'image/png', + params: { fileName: 'test.png' } + } + ] + } + ], + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.blob.addAttachments).toHaveBeenCalled() + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should remove attachments from message', async () => { + const event: Enriched = { + type: MessageEventType.AttachmentPatch, + cardId: 'card-123', + messageId: 'msg-123', + operations: [ + { + opcode: 'remove', + ids: ['att-1', 'att-2'] + } + ], + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.blob.removeAttachments).toHaveBeenCalledWith('card-123', 'blob-123', 'msg-123', ['att-1', 'att-2']) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should set attachments on message', async () => { + const event: Enriched = { + type: MessageEventType.AttachmentPatch, + cardId: 'card-123', + messageId: 'msg-123', + operations: [ + { + opcode: 'set', + attachments: [ + { + id: 'att-1', + mimeType: 'image/png', + params: { fileName: 'test.png' } + } + ] + } + ], + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.blob.setAttachments).toHaveBeenCalled() + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should update attachments on message', async () => { + const event: Enriched = { + type: MessageEventType.AttachmentPatch, + cardId: 'card-123', + messageId: 'msg-123', + operations: [ + { + opcode: 'update', + attachments: [ + { + id: 'att-1', + params: { fileName: 'updated.png' } + } + ] + } + ], + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.blob.updateAttachments).toHaveBeenCalled() + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('event - BlobPatch (deprecated)', () => { + it('should convert blob attach to attachment add', async () => { + const event: Enriched = { + type: MessageEventType.BlobPatch, + cardId: 'card-123', + messageId: 'msg-123', + operations: [ + { + opcode: 'attach', + blobs: [ + { + blobId: 'blob-1', + mimeType: 'image/png', + fileName: 'test.png', + size: 1024 + } + ] + } + ], + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.blob.addAttachments).toHaveBeenCalled() + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should convert blob detach to attachment remove', async () => { + const event: Enriched = { + type: MessageEventType.BlobPatch, + cardId: 'card-123', + messageId: 'msg-123', + operations: [ + { + opcode: 'detach', + blobIds: ['blob-1', 'blob-2'] + } + ], + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.blob.removeAttachments).toHaveBeenCalled() + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('event - ThreadPatch', () => { + it('should attach thread to message', async () => { + const event: Enriched = { + type: MessageEventType.ThreadPatch, + cardId: 'card-123', + messageId: 'msg-123', + operation: { + opcode: 'attach', + threadId: 'thread-123', + threadType: 'task' as CardType + }, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.db.attachThreadMeta).toHaveBeenCalledWith( + 'card-123', + 'msg-123', + 'thread-123', + 'task', + socialId, + event.date + ) + expect(mockClient.blob.attachThread).toHaveBeenCalled() + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should update thread on message', async () => { + const event: Enriched = { + type: MessageEventType.ThreadPatch, + cardId: 'card-123', + messageId: 'msg-123', + operation: { + opcode: 'update', + threadId: 'thread-123', + update: { repliesCount: 5 } + }, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.blob.updateThread).toHaveBeenCalledWith( + 'card-123', + 'blob-123', + 'msg-123', + 'thread-123', + { repliesCount: 5 } + ) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should add thread reply', async () => { + const event: Enriched = { + type: MessageEventType.ThreadPatch, + cardId: 'card-123', + messageId: 'msg-123', + operation: { + opcode: 'addReply', + threadId: 'thread-123' + }, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.findPersonUuid).toHaveBeenCalled() + expect(mockClient.blob.addThreadReply).toHaveBeenCalledWith( + 'card-123', + 'blob-123', + 'msg-123', + 'thread-123', + 'person-123', + event.date + ) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should remove thread reply', async () => { + const event: Enriched = { + type: MessageEventType.ThreadPatch, + cardId: 'card-123', + messageId: 'msg-123', + operation: { + opcode: 'removeReply', + threadId: 'thread-123' + }, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.findPersonUuid).toHaveBeenCalled() + expect(mockClient.blob.removeThreadReply).toHaveBeenCalledWith( + 'card-123', + 'blob-123', + 'msg-123', + 'thread-123', + 'person-123' + ) + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('event - Notification events', () => { + it('should create notification', async () => { + const event: Enriched = { + type: NotificationEventType.CreateNotification, + contextId: 'ctx-123', + messageId: 'msg-123', + blobId: 'blob-123', + notificationType: 'message', + read: false, + content: 'Test notification', + creator: socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.db.createNotification).toHaveBeenCalledWith( + 'ctx-123', + 'msg-123', + 'blob-123', + 'message', + false, + 'Test notification', + socialId, + event.date + ) + expect(event.notificationId).toBe('notif-123') + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should update notification', async () => { + const event: Enriched = { + type: NotificationEventType.UpdateNotification, + contextId: 'ctx-123', + account: accountUuid, + query: { type: 'message' }, + updates: { read: true }, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.db.updateNotification).toHaveBeenCalledWith( + { + contextId: 'ctx-123', + account: accountUuid, + type: 'message' + }, + { read: true } + ) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should skip propagate if no notifications updated', async () => { + const event: Enriched = { + type: NotificationEventType.UpdateNotification, + contextId: 'ctx-123', + account: accountUuid, + query: {}, + updates: { read: true }, + date: new Date(), + _eventExtra: {} + } + + mockClient.db.updateNotification.mockResolvedValue(0) + + await middleware.event(session, event, false) + + expect(event.skipPropagate).toBe(true) + expect(mockNext.event).not.toHaveBeenCalled() + }) + + it('should remove notifications', async () => { + const event: Enriched = { + type: NotificationEventType.RemoveNotifications, + contextId: 'ctx-123', + account: accountUuid, + ids: ['notif-1', 'notif-2'], + date: new Date(), + _eventExtra: {} + } + + mockClient.db.removeNotifications.mockResolvedValue(['notif-1', 'notif-2']) + + await middleware.event(session, event, false) + + expect(mockClient.db.removeNotifications).toHaveBeenCalledWith({ + contextId: 'ctx-123', + account: accountUuid, + id: ['notif-1', 'notif-2'] + }) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should skip propagate if no ids to remove', async () => { + const event: Enriched = { + type: NotificationEventType.RemoveNotifications, + contextId: 'ctx-123', + account: accountUuid, + ids: [], + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(event.skipPropagate).toBe(true) + expect(mockNext.event).not.toHaveBeenCalled() + }) + + it('should create notification context', async () => { + const event: Enriched = { + type: NotificationEventType.CreateNotificationContext, + account: accountUuid, + cardId: 'card-123', + lastUpdate: new Date(), + lastView: new Date(), + lastNotify: new Date(), + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.db.createNotificationContext).toHaveBeenCalled() + expect(event.contextId).toBe('ctx-123') + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should remove notification context', async () => { + const event: Enriched = { + type: NotificationEventType.RemoveNotificationContext, + contextId: 'ctx-123', + account: accountUuid, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.db.removeContext).toHaveBeenCalledWith({ + id: 'ctx-123', + account: accountUuid + }) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should skip propagate if context not found', async () => { + const event: Enriched = { + type: NotificationEventType.RemoveNotificationContext, + contextId: 'ctx-123', + account: accountUuid, + date: new Date(), + _eventExtra: {} + } + + mockClient.db.removeContext.mockResolvedValue(undefined) + + await middleware.event(session, event, false) + + expect(event.skipPropagate).toBe(true) + expect(mockNext.event).not.toHaveBeenCalled() + }) + + it('should update notification context', async () => { + const event: Enriched = { + type: NotificationEventType.UpdateNotificationContext, + contextId: 'ctx-123', + account: accountUuid, + updates: { lastView: new Date() }, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.db.updateContext).toHaveBeenCalled() + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('event - Collaborator events', () => { + it('should add collaborators', async () => { + const event: Enriched = { + type: NotificationEventType.AddCollaborators, + cardId: 'card-123', + cardType: 'task' as CardType, + collaborators: [accountUuid, 'account-456' as AccountUuid], + socialId, + date: new Date(), + _eventExtra: {} + } + + mockClient.db.addCollaborators.mockResolvedValue([accountUuid, 'account-456' as AccountUuid]) + + await middleware.event(session, event, false) + + expect(mockClient.db.addCollaborators).toHaveBeenCalledWith( + 'card-123', + 'task', + [accountUuid, 'account-456'], + event.date + ) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should skip propagate if no collaborators added', async () => { + const event: Enriched = { + type: NotificationEventType.AddCollaborators, + cardId: 'card-123', + cardType: 'task' as CardType, + collaborators: [accountUuid], + socialId, + date: new Date(), + _eventExtra: {} + } + + mockClient.db.addCollaborators.mockResolvedValue([]) + + await middleware.event(session, event, false) + + expect(event.skipPropagate).toBe(true) + expect(mockNext.event).not.toHaveBeenCalled() + }) + + it('should remove collaborators', async () => { + const event: Enriched = { + type: NotificationEventType.RemoveCollaborators, + cardId: 'card-123', + cardType: 'task' as CardType, + collaborators: [accountUuid], + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.db.removeCollaborators).toHaveBeenCalledWith({ + cardId: 'card-123', + account: [accountUuid] + }) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should skip propagate if no collaborators to remove', async () => { + const event: Enriched = { + type: NotificationEventType.RemoveCollaborators, + cardId: 'card-123', + cardType: 'task' as CardType, + collaborators: [], + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(event.skipPropagate).toBe(true) + expect(mockNext.event).not.toHaveBeenCalled() + }) + }) + + describe('event - Label events', () => { + it('should create label', async () => { + const event: Enriched = { + type: LabelEventType.CreateLabel, + cardId: 'card-123', + cardType: 'task' as CardType, + labelId: 'label-123', + account: accountUuid, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.db.createLabel).toHaveBeenCalledWith( + 'card-123', + 'task', + 'label-123', + accountUuid, + event.date + ) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should remove label', async () => { + const event: Enriched = { + type: LabelEventType.RemoveLabel, + labelId: 'label-123', + cardId: 'card-123', + account: accountUuid, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.db.removeLabels).toHaveBeenCalledWith({ + labelId: 'label-123', + cardId: 'card-123', + account: accountUuid + }) + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('event - Peer events', () => { + it('should create peer', async () => { + const event: Enriched = { + type: PeerEventType.CreatePeer, + workspaceId: workspace, + cardId: 'card-123', + kind: 'slack', + value: 'channel-123', + extra: { foo: 'bar' }, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.db.createPeer).toHaveBeenCalledWith( + workspace, + 'card-123', + 'slack', + 'channel-123', + { foo: 'bar' }, + event.date + ) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should remove peer', async () => { + const event: Enriched = { + type: PeerEventType.RemovePeer, + workspaceId: workspace, + cardId: 'card-123', + kind: 'slack', + value: 'channel-123', + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockClient.db.removePeer).toHaveBeenCalledWith( + workspace, + 'card-123', + 'slack', + 'channel-123' + ) + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('event - Card events', () => { + it('should handle UpdateCardType event', async () => { + const event: Enriched = { + type: CardEventType.UpdateCardType, + cardId: 'card-123', + cardType: 'issue' as CardType, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should handle RemoveCard event', async () => { + const event: Enriched = { + type: CardEventType.RemoveCard, + cardId: 'card-123', + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('close', () => { + it('should close database connection', () => { + middleware.close() + + expect(mockClient.db.close).toHaveBeenCalled() + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/triggers.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/triggers.test.ts new file mode 100644 index 0000000000..a16efe8a72 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/middleware/triggers.test.ts @@ -0,0 +1,891 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { type MeasureContext, type WorkspaceUuid } from '@hcengineering/core' +import { + MessageEventType, + type SessionData, + NotificationEventType +} from '@hcengineering/communication-sdk-types' +import { type AccountUuid, type CardType, type Markdown, type SocialID } from '@hcengineering/communication-types' + +import { TriggersMiddleware } from '../../middleware/triggers' +import { type Enriched, type MiddlewareContext, type CommunicationCallbacks, Middleware } from '../../types' +import { type LowLevelClient } from '../../client' +import { notify } from '../../notification/notification' + +// Mock external dependencies +jest.mock('../../triggers/all', () => []) +jest.mock('../../notification/notification', () => ({ + notify: jest.fn().mockResolvedValue([]) +})) + +describe('TriggersMiddleware', () => { + // Test fixtures + let mockContext: MiddlewareContext + let mockClient: jest.Mocked + let mockMeasureCtx: jest.Mocked + let mockNext: any + let mockCallbacks: jest.Mocked + let session: SessionData + let middleware: TriggersMiddleware + + // Test constants + const workspace = 'test-workspace' as WorkspaceUuid + const accountUuid = 'account-123' as AccountUuid + const socialId = 'social-123' as SocialID + + beforeEach(() => { + jest.clearAllMocks() + + // Setup mock context + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis(), + contextData: undefined + } as any as jest.Mocked + + // Setup mock client + mockClient = { + db: { + findNotificationContexts: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findThreadMeta: jest.fn().mockResolvedValue([]), + getAccountsByPersonIds: jest.fn().mockResolvedValue([]) + }, + blob: {}, + findPersonUuid: jest.fn().mockResolvedValue('person-123') + } as unknown as jest.Mocked + + // Setup mock middleware chain + mockNext = { + event: jest.fn().mockResolvedValue({}), + handleBroadcast: jest.fn(), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + closeSession: jest.fn(), + close: jest.fn() + } as any as jest.Mocked + + // Setup mock callbacks + mockCallbacks = { + registerAsyncRequest: jest.fn((ctx, fn) => { + void fn(ctx).catch(() => {}) + return undefined + }), + broadcast: jest.fn(), + enqueue: jest.fn() + } as any as jest.Mocked + + // Setup middleware context + mockContext = { + ctx: mockMeasureCtx, + client: mockClient, + workspace, + metadata: { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'secret', + messagesPerBlob: 100 + }, + cadsWithPeers: new Set(), + head: mockNext + } + + // Setup test session + session = { + account: { + uuid: accountUuid, + socialIds: [socialId] + }, + sessionId: 'session-123', + asyncData: [] + } as any as SessionData + + middleware = new TriggersMiddleware(mockCallbacks, mockContext, mockNext) + }) + + describe('event', () => { + it('should process event and call next middleware', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test message' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + const result = await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + expect(result).toBeDefined() + }) + + it('should skip propagation if event has skipPropagate flag', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + skipPropagate: true, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should handle derived events', async () => { + const event: Enriched = { + type: MessageEventType.UpdatePatch, + cardId: 'card-123', + messageId: 'msg-123', + content: 'Updated' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, true) + + expect(mockNext.event).toHaveBeenCalledWith(session, event, true) + }) + + it('should register async request for non-derived events', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + session.contextData = { foo: 'bar' } + + await middleware.event(session, event, false) + + expect(mockCallbacks.registerAsyncRequest).toHaveBeenCalled() + expect(session.isAsyncContext).toBe(true) + }) + + it('should not register duplicate async requests', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + session.contextData = { foo: 'bar' } + session.isAsyncContext = true + + await middleware.event(session, event, false) + + expect(mockCallbacks.registerAsyncRequest).not.toHaveBeenCalled() + }) + + it('should handle events without context data', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockCallbacks.registerAsyncRequest).not.toHaveBeenCalled() + }) + + it('should sort async data by date', async () => { + const date1 = new Date('2025-01-01T10:00:00Z') + const date2 = new Date('2025-01-01T09:00:00Z') + const date3 = new Date('2025-01-01T11:00:00Z') + + const event1: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-1', + date: date1, + _eventExtra: {} + } + + const event2: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-2', + date: date2, + _eventExtra: {} + } + + const event3: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-3', + date: date3, + _eventExtra: {} + } + + session.asyncData = [event1, event2, event3] + + await middleware.event(session, event1, false) + + expect(session.asyncData).toBeDefined() + }) + }) + + describe('processDerived', () => { + it('should process derived events', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.processDerived(session, [event], true) + + expect(mockCallbacks.registerAsyncRequest).not.toHaveBeenCalled() + }) + + it('should register async request for non-derived events with context data', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + session.contextData = { foo: 'bar' } + + await middleware.processDerived(session, [event], false) + + expect(mockCallbacks.registerAsyncRequest).toHaveBeenCalled() + expect(session.isAsyncContext).toBe(true) + }) + + it('should handle multiple events', async () => { + const event1: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-1', + date: new Date(), + _eventExtra: {} + } + + const event2: Enriched = { + type: MessageEventType.UpdatePatch, + cardId: 'card-123', + messageId: 'msg-2', + content: 'Updated' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.processDerived(session, [event1, event2], false) + + expect(session.asyncData).toBeDefined() + }) + + it('should filter out events with skipPropagate in asyncData', async () => { + const event1: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123' as any, + messageId: 'msg-1', + date: new Date(), + skipPropagate: false, + _eventExtra: {} + } + + const event2: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-456' as any, + messageId: 'msg-2', + date: new Date(), + skipPropagate: true, + _eventExtra: {} + } + + await middleware.processDerived(session, [event1, event2], false) + + expect(session.asyncData).toBeDefined() + }) + }) + + describe('notification events', () => { + it('should handle notification context events', async () => { + const event: Enriched = { + type: NotificationEventType.CreateNotificationContext, + account: accountUuid, + cardId: 'card-123', + lastUpdate: new Date(), + lastView: new Date(), + lastNotify: new Date(), + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should handle update notification events', async () => { + const event: Enriched = { + type: NotificationEventType.UpdateNotification, + contextId: 'ctx-123', + account: accountUuid, + query: { type: 'message' }, + updates: { read: true }, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('async context handling', () => { + it('should handle async context correctly', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + session.isAsyncContext = true + + await middleware.event(session, event, false) + + expect(mockCallbacks.registerAsyncRequest).not.toHaveBeenCalled() + }) + + it('should clear asyncData after processing non-async events', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + session.asyncData = [ + { + type: MessageEventType.CreateMessage, + cardId: 'card-456' as any, + messageId: 'msg-456' as any, + cardType: 'task' as CardType, + messageType: 'text' as any, + content: 'Test' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + ] + + await middleware.event(session, event, false) + + expect(session.asyncData).toEqual([]) + }) + + it('should preserve asyncData in async context', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + session.isAsyncContext = true + session.asyncData = [ + { + type: MessageEventType.CreateMessage, + cardId: 'card-456' as any, + messageId: 'msg-456' as any, + cardType: 'task' as CardType, + messageType: 'text' as any, + content: 'Test' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + ] + + const initialAsyncDataLength = session.asyncData.length + + await middleware.event(session, event, false) + + expect(session.asyncData.length).toBeGreaterThanOrEqual(initialAsyncDataLength) + }) + }) + + describe('error handling', () => { + it('should handle errors in trigger execution', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + mockNext.event.mockRejectedValue(new Error('Trigger error')) + + await expect(middleware.event(session, event, false)).rejects.toThrow('Trigger error') + }) + + it('should handle errors in async context', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + session.contextData = { foo: 'bar' } + + let asyncCallbackPromise: Promise | undefined + mockCallbacks.registerAsyncRequest.mockImplementation((ctx, fn) => { + asyncCallbackPromise = fn(ctx) + return undefined + }) + + const notifyMock = notify as jest.MockedFunction + notifyMock.mockRejectedValueOnce(new Error('Async error')) + + await middleware.event(session, event, false) + + expect(mockCallbacks.registerAsyncRequest).toHaveBeenCalled() + + if (asyncCallbackPromise !== undefined) { + await expect(asyncCallbackPromise).rejects.toThrow('Async error') + } + }) + }) + + describe('edge cases', () => { + it('should handle empty asyncData array', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + session.asyncData = [] + + await middleware.event(session, event, false) + + expect(session.asyncData).toEqual([]) + }) + + it('should handle undefined asyncData', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + session.asyncData = undefined as any + + await middleware.event(session, event, false) + + expect(session.asyncData).toBeDefined() + }) + + it('should handle events without head middleware', async () => { + const contextWithoutHead = { ...mockContext, head: undefined } + const middlewareWithoutHead = new TriggersMiddleware(mockCallbacks, contextWithoutHead, mockNext) + + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + await middlewareWithoutHead.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should handle events with null values', async () => { + const event: Enriched = { + type: MessageEventType.UpdatePatch, + cardId: 'card-123', + messageId: 'msg-123', + content: null as any, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should process events with skipPropagate', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + skipPropagate: true, + date: new Date(), + _eventExtra: {} + } + + const result = await middleware.event(session, event, false) + + expect(result).toBeDefined() + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should handle very large asyncData arrays', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + session.asyncData = Array.from({ length: 1000 }, (_, i) => ({ + type: MessageEventType.CreateMessage, + cardId: `card-${i}`, + messageId: `msg-${i}`, + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + })) as any + + await middleware.event(session, event, false) + + expect(session.asyncData).toBeDefined() + }) + + it('should handle events with different date formats', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date('2025-01-01T00:00:00.000Z'), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should handle concurrent event processing', async () => { + const event1: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-1', + messageId: 'msg-1', + date: new Date(), + _eventExtra: {} + } + + const event2: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-2', + messageId: 'msg-2', + date: new Date(), + _eventExtra: {} + } + + await Promise.all([ + middleware.event(session, event1, false), + middleware.event(session, event2, false) + ]) + + expect(mockNext.event).toHaveBeenCalledTimes(2) + }) + + it('should preserve event order in asyncData', async () => { + const dates = [ + new Date('2025-01-01T10:00:00Z'), + new Date('2025-01-01T09:00:00Z'), + new Date('2025-01-01T11:00:00Z') + ] + + const events = dates.map((date, i) => ({ + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: `msg-${i}`, + date, + _eventExtra: {} + })) + + for (const event of events) { + await middleware.event(session, event as Enriched, false) + } + + expect(session.asyncData).toBeDefined() + }) + }) + + describe('trigger integration', () => { + it('should execute triggers for message events', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test with @mention' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should execute triggers for notification events', async () => { + const event: Enriched = { + type: NotificationEventType.CreateNotification, + contextId: 'ctx-123', + messageId: 'msg-123', + blobId: 'blob-123', + notificationType: 'message', + content: 'Test notification', + creator: socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should handle multiple trigger results', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test message' as Markdown, + socialId, + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('async request management', () => { + it('should register only one async request per session', async () => { + const event1: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-1', + messageId: 'msg-1', + date: new Date(), + _eventExtra: {} + } + + const event2: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-2', + messageId: 'msg-2', + date: new Date(), + _eventExtra: {} + } + + session.contextData = { foo: 'bar' } + + await middleware.event(session, event1, false) + await middleware.event(session, event2, false) + + expect(mockCallbacks.registerAsyncRequest).toHaveBeenCalledTimes(1) + }) + + it('should handle async request with contextData', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + session.contextData = { userId: 'user-123', requestId: 'req-456' } + + await middleware.event(session, event, false) + + expect(mockCallbacks.registerAsyncRequest).toHaveBeenCalled() + expect(session.isAsyncContext).toBe(true) + }) + + it('should execute async callback immediately in tests', async () => { + let callbackExecuted = false + mockCallbacks.registerAsyncRequest.mockImplementation((ctx, fn) => { + callbackExecuted = true + void fn(ctx) + return undefined + }) + + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + session.contextData = { foo: 'bar' } + + await middleware.event(session, event, false) + + expect(callbackExecuted).toBe(true) + }) + }) + + describe('broadcast handling', () => { + it('should call handleBroadcast after event processing', async () => { + const handleBroadcastSpy = jest.spyOn(middleware, 'handleBroadcast') + + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(handleBroadcastSpy).toHaveBeenCalled() + }) + + it('should handle broadcast with multiple events', async () => { + const events: Enriched[] = [ + { + type: MessageEventType.CreateMessage, + cardId: 'card-1', + messageId: 'msg-1', + date: new Date(), + _eventExtra: {} + }, + { + type: MessageEventType.CreateMessage, + cardId: 'card-2', + messageId: 'msg-2', + date: new Date(), + _eventExtra: {} + } + ] + + middleware.handleBroadcast(session, events) + + expect(mockNext.handleBroadcast).toHaveBeenCalledWith(session, events) + }) + + it('should handle empty broadcast', async () => { + middleware.handleBroadcast(session, []) + + expect(mockNext.handleBroadcast).toHaveBeenCalledWith(session, []) + }) + }) + + describe('performance and memory', () => { + it('should handle rapid succession of events', async () => { + const events = Array.from({ length: 100 }, (_, i) => ({ + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: `msg-${i}`, + date: new Date(), + _eventExtra: {} + })) + + for (const event of events) { + await middleware.event(session, event as Enriched, false) + } + + expect(mockNext.event).toHaveBeenCalledTimes(100) + }) + + it('should clean up processed peers events', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + date: new Date(), + _eventExtra: {} + } + + await middleware.event(session, event, false) + + expect(session.asyncData).toEqual([]) + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/validate.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/validate.test.ts new file mode 100644 index 0000000000..f5b03fc829 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/middleware/validate.test.ts @@ -0,0 +1,1028 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' +import { MessageEventType, NotificationEventType, SessionData } from '@hcengineering/communication-sdk-types' +import { AccountUuid, CardID, CardType, Markdown, SocialID } from '@hcengineering/communication-types' + +import { ValidateMiddleware } from '../../middleware/validate' +import { Enriched, Middleware, MiddlewareContext } from '../../types' +import { LowLevelClient } from '../../client' + +describe('ValidateMiddleware', () => { + let mockContext: MiddlewareContext + let mockClient: jest.Mocked + let mockMeasureCtx: jest.Mocked + let mockNext: any + let session: SessionData + let middleware: ValidateMiddleware + + const workspace = 'test-workspace' as WorkspaceUuid + const accountUuid = 'account-123' as AccountUuid + const socialId = 'social-123' as SocialID + + beforeEach(() => { + jest.clearAllMocks() + + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis() + } as any as jest.Mocked + + mockClient = { + db: {}, + blob: {} + } as unknown as jest.Mocked + + mockNext = { + event: jest.fn().mockResolvedValue({}), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + handleBroadcast: jest.fn(), + closeSession: jest.fn(), + close: jest.fn() + } as any as jest.Mocked + + mockContext = { + ctx: mockMeasureCtx, + client: mockClient, + workspace, + metadata: { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'secret', + messagesPerBlob: 100 + }, + cadsWithPeers: new Set() + } + + session = { + account: { + uuid: accountUuid, + socialIds: [socialId] + }, + sessionId: 'session-123' + } as any as SessionData + + middleware = new ValidateMiddleware(mockContext, mockNext) + }) + + describe('event - CreateMessage validation', () => { + it('should validate correct CreateMessage event', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test message' as Markdown, + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should reject CreateMessage without cardId', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + + it('should reject CreateMessage without content', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + messageType: 'text', + socialId, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + + it('should reject CreateMessage without socialId', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123' as CardID, + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + }) + + describe('event - UpdatePatch validation', () => { + it('should validate correct UpdatePatch event', async () => { + const event: Enriched = { + type: MessageEventType.UpdatePatch, + cardId: 'card-123' as CardID, + messageId: 'msg-123', + content: 'Updated' as Markdown, + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalledWith(session, event, false) + }) + + it('should validate UpdatePatch without messageId', async () => { + const event: Enriched = { + type: MessageEventType.UpdatePatch, + cardId: 'card-123' as CardID, + content: 'Updated' as Markdown, + socialId, + date: new Date() + } + + // UpdatePatch can have optional messageId according to schema + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('event - RemovePatch validation', () => { + it('should validate correct RemovePatch event', async () => { + const event: Enriched = { + type: MessageEventType.RemovePatch, + cardId: 'card-123' as CardID, + messageId: 'msg-123', + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject RemovePatch without cardId', async () => { + const event: Enriched = { + type: MessageEventType.RemovePatch, + messageId: 'msg-123', + socialId, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + }) + + describe('event - ReactionPatch validation', () => { + it('should validate correct ReactionPatch add event', async () => { + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId: 'card-123' as CardID, + messageId: 'msg-123', + operation: { + opcode: 'add', + reaction: '👍' + }, + personUuid: 'person-123', + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate correct ReactionPatch remove event', async () => { + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId: 'card-123' as CardID, + messageId: 'msg-123', + operation: { + opcode: 'remove', + reaction: '👍' + }, + personUuid: 'person-123', + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject ReactionPatch without operation', async () => { + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + personUuid: 'person-123', + socialId, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + + it('should reject ReactionPatch with invalid opcode', async () => { + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operation: { + opcode: 'invalid', + reaction: '👍' + }, + personUuid: 'person-123', + socialId, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + }) + + describe('event - AttachmentPatch validation', () => { + it('should validate correct AttachmentPatch add event', async () => { + const event: Enriched = { + type: MessageEventType.AttachmentPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operations: [ + { + opcode: 'add', + attachments: [ + { + id: '550e8400-e29b-41d4-a716-446655440000', + mimeType: 'application/blob', + params: { + blobId: '550e8400-e29b-41d4-a716-446655440001', + mimeType: 'image/png', + fileName: 'test.png', + size: 1024 + } + } + ] + } + ], + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate AttachmentPatch remove event', async () => { + const event: Enriched = { + type: MessageEventType.AttachmentPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operations: [ + { + opcode: 'remove', + ids: ['550e8400-e29b-41d4-a716-446655440000'] + } + ], + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate AttachmentPatch set event', async () => { + const event: Enriched = { + type: MessageEventType.AttachmentPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operations: [ + { + opcode: 'set', + attachments: [ + { + id: '550e8400-e29b-41d4-a716-446655440000', + mimeType: 'application/blob', + params: { + blobId: '550e8400-e29b-41d4-a716-446655440002', + mimeType: 'image/png', + fileName: 'file.png', + size: 2048 + } + } + ] + } + ], + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate AttachmentPatch update event', async () => { + const event: Enriched = { + type: MessageEventType.AttachmentPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operations: [ + { + opcode: 'update', + attachments: [ + { + id: '550e8400-e29b-41d4-a716-446655440000', + params: { newData: 'value' } + } + ] + } + ], + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject AttachmentPatch with empty operations', async () => { + const event: Enriched = { + type: MessageEventType.AttachmentPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operations: [], + socialId, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + + it('should reject AttachmentPatch without operations', async () => { + const event: Enriched = { + type: MessageEventType.AttachmentPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + socialId, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + + it('should validate AttachmentPatch with link preview', async () => { + const event: Enriched = { + type: MessageEventType.AttachmentPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operations: [ + { + opcode: 'add', + attachments: [ + { + id: '550e8400-e29b-41d4-a716-446655440000', + mimeType: 'application/vnd.huly.link-preview', + params: { + url: 'https://example.com', + host: 'example.com' + } + } + ] + } + ], + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('event - ThreadPatch validation', () => { + it('should validate correct ThreadPatch attach event', async () => { + const event: Enriched = { + type: MessageEventType.ThreadPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operation: { + opcode: 'attach', + threadId: 'thread-123', + threadType: 'task' as CardType + }, + personUuid: 'person-123', + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject ThreadPatch without operation', async () => { + const event: Enriched = { + type: MessageEventType.ThreadPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + personUuid: 'person-123', + socialId, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + + it('should reject ThreadPatch without personUuid', async () => { + const event: Enriched = { + type: MessageEventType.ThreadPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operation: { + opcode: 'attach', + threadId: 'thread-123', + threadType: 'task' as CardType + }, + socialId, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + }) + + describe('event - BlobPatch validation (deprecated)', () => { + it('should validate correct BlobPatch attach event', async () => { + const event: Enriched = { + type: MessageEventType.BlobPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operations: [ + { + opcode: 'attach', + blobs: [ + { + blobId: '550e8400-e29b-41d4-a716-446655440000', + mimeType: 'image/png', + fileName: 'test.png', + size: 1024 + } + ] + } + ], + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate BlobPatch detach event', async () => { + const event: Enriched = { + type: MessageEventType.BlobPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operations: [ + { + opcode: 'detach', + blobIds: ['550e8400-e29b-41d4-a716-446655440000'] + } + ], + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate BlobPatch set event', async () => { + const event: Enriched = { + type: MessageEventType.BlobPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operations: [ + { + opcode: 'set', + blobs: [ + { + blobId: '550e8400-e29b-41d4-a716-446655440000', + mimeType: 'application/pdf', + fileName: 'doc.pdf', + size: 2048 + } + ] + } + ], + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate BlobPatch update event', async () => { + const event: Enriched = { + type: MessageEventType.BlobPatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + operations: [ + { + opcode: 'update', + blobs: [ + { + blobId: '550e8400-e29b-41d4-a716-446655440000', + fileName: 'updated.png' + } + ] + } + ], + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('event - Notification events validation', () => { + it('should validate UpdateNotification event', async () => { + const event: Enriched = { + type: NotificationEventType.UpdateNotification, + contextId: 'ctx-123' as any, + account: accountUuid, + query: { + type: 'message' + }, + updates: { + read: true + }, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate RemoveNotificationContext event', async () => { + const event: Enriched = { + type: NotificationEventType.RemoveNotificationContext, + contextId: 'ctx-123' as any, + account: accountUuid, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate UpdateNotificationContext event', async () => { + const event: Enriched = { + type: NotificationEventType.UpdateNotificationContext, + contextId: 'ctx-123' as any, + account: accountUuid, + updates: { + lastView: new Date() + }, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate AddCollaborators event', async () => { + const event: Enriched = { + type: NotificationEventType.AddCollaborators, + cardId: 'card-123' as any, + cardType: 'task' as CardType, + collaborators: [accountUuid, 'account-456' as AccountUuid], + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject AddCollaborators with empty collaborators array', async () => { + const event: Enriched = { + type: NotificationEventType.AddCollaborators, + cardId: 'card-123' as any, + cardType: 'task' as CardType, + collaborators: [], + socialId, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + + it('should validate RemoveCollaborators event', async () => { + const event: Enriched = { + type: NotificationEventType.RemoveCollaborators, + cardId: 'card-123' as any, + cardType: 'task' as CardType, + collaborators: [accountUuid], + socialId, + date: new Date() + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should reject RemoveCollaborators with empty collaborators array', async () => { + const event: Enriched = { + type: NotificationEventType.RemoveCollaborators, + cardId: 'card-123' as any, + cardType: 'task' as CardType, + collaborators: [], + socialId, + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + }) + }) + + describe('event - CreateMessage with options', () => { + it('should validate CreateMessage with skipLinkPreviews option', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123' as any, + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date(), + options: { + skipLinkPreviews: true + } + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate CreateMessage with noNotify option', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123' as any, + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date(), + options: { + noNotify: true + } + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate CreateMessage with ignoreMentions option', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123' as any, + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test @user' as Markdown, + socialId, + date: new Date(), + options: { + ignoreMentions: true + } + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate CreateMessage with language', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123' as any, + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date(), + language: 'en' + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate CreateMessage with extra data', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId: 'card-123' as any, + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date(), + extra: { + customField: 'value' + } + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('event - UpdatePatch with options', () => { + it('should validate UpdatePatch with skipLinkPreviewsUpdate option', async () => { + const event: Enriched = { + type: MessageEventType.UpdatePatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + content: 'Updated' as Markdown, + socialId, + date: new Date(), + options: { + skipLinkPreviewsUpdate: true + } + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + + it('should validate UpdatePatch with extra data', async () => { + const event: Enriched = { + type: MessageEventType.UpdatePatch, + cardId: 'card-123' as any, + messageId: 'msg-123', + content: 'Updated' as Markdown, + socialId, + date: new Date(), + extra: { + customField: 'newValue' + } + } + + await middleware.event(session, event, false) + expect(mockNext.event).toHaveBeenCalled() + }) + }) + + describe('event - derived events', () => { + it('should skip validation for derived events', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + // Missing required fields + date: new Date() + } + + await middleware.event(session, event, true) + + expect(mockNext.event).toHaveBeenCalledWith(session, event, true) + }) + }) + + describe('findNotificationContexts', () => { + it('should validate correct params', async () => { + const params = { cardId: 'card-123' as any } + + await middleware.findNotificationContexts(session, params) + + expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(session, params, undefined) + }) + + it('should validate params with multiple fields', async () => { + const params = { + cardId: 'card-123' as any, + account: accountUuid, + limit: 10 + } + + await middleware.findNotificationContexts(session, params) + + expect(mockNext.findNotificationContexts).toHaveBeenCalled() + }) + + it('should pass subscription parameter', async () => { + const params = { cardId: 'card-123' as any } + const subscription = 'sub-123' + + await middleware.findNotificationContexts(session, params, subscription) + + expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(session, params, subscription) + }) + + it('should validate params with array of cardIds', async () => { + const params = { + cardId: ['card-1', 'card-2', 'card-3'] as any[] + } + + await middleware.findNotificationContexts(session, params) + expect(mockNext.findNotificationContexts).toHaveBeenCalled() + }) + + it('should validate params with array of accounts', async () => { + const params = { + cardId: 'card-123' as any, + account: [accountUuid, 'account-456' as AccountUuid] + } + + await middleware.findNotificationContexts(session, params) + expect(mockNext.findNotificationContexts).toHaveBeenCalled() + }) + + it('should validate params with notifications nested params', async () => { + const params = { + cardId: 'card-123' as any, + notifications: { + limit: 10, + order: 1, + read: false, + total: true + } + } + + await middleware.findNotificationContexts(session, params) + expect(mockNext.findNotificationContexts).toHaveBeenCalled() + }) + + it('should validate params with order parameter', async () => { + const params = { + cardId: 'card-123' as any, + order: 1, + limit: 20 + } + + await middleware.findNotificationContexts(session, params) + expect(mockNext.findNotificationContexts).toHaveBeenCalled() + }) + }) + + describe('findNotifications', () => { + it('should validate correct params', async () => { + const params = { contextId: 'ctx-123' as any } + + await middleware.findNotifications(session, params) + + expect(mockNext.findNotifications).toHaveBeenCalledWith(session, params, undefined) + }) + + it('should validate params with query', async () => { + const params = { + contextId: 'ctx-123' as any, + read: false, + limit: 20 + } + + await middleware.findNotifications(session, params) + + expect(mockNext.findNotifications).toHaveBeenCalled() + }) + + it('should validate params with all optional fields', async () => { + const params = { + contextId: 'ctx-123' as any, + read: false, + cardId: 'card-123' as any, + total: true, + limit: 50, + order: 1 + } + + await middleware.findNotifications(session, params) + expect(mockNext.findNotifications).toHaveBeenCalled() + }) + + it('should validate params without contextId', async () => { + const params = { + account: accountUuid, + cardId: 'card-123' as any + } + + await middleware.findNotifications(session, params) + expect(mockNext.findNotifications).toHaveBeenCalled() + }) + }) + + describe('findLabels', () => { + it('should validate correct params', async () => { + const params = { cardId: 'card-123' as any } + + await middleware.findLabels(session, params) + + expect(mockNext.findLabels).toHaveBeenCalledWith(session, params, undefined) + }) + + it('should validate params with labelId', async () => { + const params = { + cardId: 'card-123' as any, + labelId: 'label-456' as any + } + + await middleware.findLabels(session, params) + + expect(mockNext.findLabels).toHaveBeenCalled() + }) + + it('should validate params with array of labelIds', async () => { + const params = { + cardId: 'card-123' as any, + labelId: ['label-1', 'label-2'] as any + } + + await middleware.findLabels(session, params) + expect(mockNext.findLabels).toHaveBeenCalled() + }) + + it('should validate params with cardType', async () => { + const params = { + cardId: 'card-123' as any, + cardType: 'task' as CardType + } + + await middleware.findLabels(session, params) + expect(mockNext.findLabels).toHaveBeenCalled() + }) + + it('should validate params with array of cardTypes', async () => { + const params = { + cardId: 'card-123' as any, + cardType: ['task', 'issue'] as CardType[] + } + + await middleware.findLabels(session, params) + expect(mockNext.findLabels).toHaveBeenCalled() + }) + }) + + describe('findCollaborators', () => { + it('should validate correct params', async () => { + const params = { cardId: 'card-123' as CardID } + + await middleware.findCollaborators(session, params) + + expect(mockNext.findCollaborators).toHaveBeenCalledWith(session, params) + }) + }) + + describe('findMessagesGroups', () => { + it('should validate correct params', async () => { + const paramsRaw = { cardId: 'card-123' as CardID, fromDate: { less: '2025-10-20T18:59:17.593Z' as any } } + const params = { cardId: 'card-123' as CardID, fromDate: { less: new Date('2025-10-20T18:59:17.593Z') } } + + await middleware.findMessagesGroups(session, paramsRaw) + + expect(mockNext.findMessagesGroups).toHaveBeenCalledWith(session, params) + }) + }) + + describe('validation error handling', () => { + it('should log validation errors', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + // Missing required fields + date: new Date() + } + + await expect(middleware.event(session, event, false)).rejects.toThrow(Error) + + expect(mockMeasureCtx.error).toHaveBeenCalled() + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/middlewares.test.ts b/foundations/communication/packages/server/src/__tests__/middlewares.test.ts new file mode 100644 index 0000000000..eab78e1572 --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/middlewares.test.ts @@ -0,0 +1,644 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' +import { MessageEventType, SessionData } from '@hcengineering/communication-sdk-types' +import { AccountUuid, CardType, Markdown, SocialID } from '@hcengineering/communication-types' +import { buildMiddlewares, Middlewares } from '../middlewares' +import { Metadata, MiddlewareContext, MiddlewareCreateFn, CommunicationCallbacks } from '../types' +import { LowLevelClient } from '../client' + +describe('Middlewares', () => { + let mockContext: MiddlewareContext + let mockClient: { + db: { + findPeers: jest.Mock + findMessagesMeta: jest.Mock + findNotificationContexts: jest.Mock + findNotifications: jest.Mock + findLabels: jest.Mock + findCollaborators: jest.Mock + } + blob: Record + } + let mockMeasureCtx: jest.Mocked + let mockCallbacks: jest.Mocked + let session: SessionData + let metadata: Metadata + + const workspace = 'test-workspace' as WorkspaceUuid + const accountUuid = 'account-123' as AccountUuid + const socialId = 'social-123' as SocialID + + beforeEach(() => { + jest.clearAllMocks() + + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis(), + contextData: undefined + } as unknown as jest.Mocked + + mockClient = { + db: { + findPeers: jest.fn().mockResolvedValue([]), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]) + }, + blob: {} + } + + mockCallbacks = { + registerAsyncRequest: jest.fn(), + broadcast: jest.fn(), + enqueue: jest.fn() + } as any as jest.Mocked + + metadata = { + accountsUrl: 'http://accounts.test', + hulylakeUrl: 'http://hulylake.test', + secret: 'test-secret', + messagesPerBlob: 100 + } + + mockContext = { + ctx: mockMeasureCtx, + client: mockClient as unknown as LowLevelClient, + workspace, + metadata, + cadsWithPeers: new Set() + } + + session = { + account: { + uuid: accountUuid, + socialIds: [socialId] + }, + sessionId: 'session-123', + asyncData: [] + } as unknown as SessionData + }) + + describe('buildMiddlewares', () => { + it('should build middleware chain with all middlewares', async () => { + const middlewares = await buildMiddlewares( + mockMeasureCtx, + workspace, + metadata, + mockClient as unknown as LowLevelClient, + mockCallbacks + ) + + expect(middlewares).toBeDefined() + expect(mockClient.db.findPeers).toHaveBeenCalledWith({ workspaceId: workspace }) + }) + + it('should initialize cadsWithPeers from database', async () => { + const peers = [ + { cardId: 'card-1' as any, workspaceId: workspace, kind: 'slack', value: 'channel-1' }, + { cardId: 'card-2' as any, workspaceId: workspace, kind: 'slack', value: 'channel-2' } + ] + mockClient.db.findPeers.mockResolvedValue(peers as any) + + await buildMiddlewares( + mockMeasureCtx, + workspace, + metadata, + mockClient as unknown as LowLevelClient, + mockCallbacks + ) + + expect(mockClient.db.findPeers).toHaveBeenCalledWith({ workspaceId: workspace }) + }) + + it('should handle errors during middleware initialization', async () => { + mockClient.db.findPeers.mockRejectedValue(new Error('Database error')) + + await expect( + buildMiddlewares(mockMeasureCtx, workspace, metadata, mockClient as unknown as LowLevelClient, mockCallbacks) + ).rejects.toThrow('Database error') + }) + }) + + describe('Middlewares.create', () => { + it('should create middleware chain from create functions', async () => { + const createFns: MiddlewareCreateFn[] = [ + async (context, next) => + ({ + ...next, + event: jest.fn().mockResolvedValue({}), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + handleBroadcast: jest.fn(), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + close: jest.fn(), + closeSession: jest.fn() + }) as any + ] + + const middlewares = await Middlewares.create(mockMeasureCtx, mockContext, createFns) + + expect(middlewares).toBeDefined() + }) + + it('should handle errors during chain building', async () => { + const createFns: MiddlewareCreateFn[] = [ + async () => { + throw new Error('Middleware initialization failed') + } + ] + + await expect(Middlewares.create(mockMeasureCtx, mockContext, createFns)).rejects.toThrow( + 'Middleware initialization failed' + ) + + expect(mockMeasureCtx.error).toHaveBeenCalledWith( + 'failed to initialize middlewares', + expect.objectContaining({ + workspace + }) + ) + }) + + it('should build chain in correct order', async () => { + const order: string[] = [] + const createFns: MiddlewareCreateFn[] = [ + async (context, next) => { + order.push('first') + return { + ...next, + event: jest.fn().mockResolvedValue({}), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + handleBroadcast: jest.fn(), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + close: jest.fn(), + closeSession: jest.fn() + } as any + }, + async (context, next) => { + order.push('second') + return { + ...next, + event: jest.fn().mockResolvedValue({}), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + handleBroadcast: jest.fn(), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + close: jest.fn(), + closeSession: jest.fn() + } as any + } + ] + + await Middlewares.create(mockMeasureCtx, mockContext, createFns) + + // Middlewares should be created in reverse order (last to first) + expect(order).toEqual(['second', 'first']) + }) + }) + + describe('Middlewares methods', () => { + let middlewares: Middlewares + let mockHead: any + + beforeEach(async () => { + mockHead = { + event: jest.fn().mockResolvedValue({}), + findMessagesMeta: jest.fn().mockResolvedValue([{ messageId: 'msg-1' }]), + findMessagesGroups: jest.fn().mockResolvedValue([{ blobId: 'blob-1' }]), + findNotificationContexts: jest.fn().mockResolvedValue([{ id: 'ctx-1' }]), + findNotifications: jest.fn().mockResolvedValue([{ id: 'notif-1' }]), + findLabels: jest.fn().mockResolvedValue([{ labelId: 'label-1' }]), + findCollaborators: jest.fn().mockResolvedValue([{ account: accountUuid }]), + findPeers: jest.fn().mockResolvedValue([{ cardId: 'card-1' }]), + handleBroadcast: jest.fn(), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + close: jest.fn(), + closeSession: jest.fn() + } + + const createFns: MiddlewareCreateFn[] = [async () => mockHead] + + middlewares = await Middlewares.create(mockMeasureCtx, mockContext, createFns) + }) + + describe('findMessagesMeta', () => { + it('should delegate to head middleware', async () => { + const params = { cardId: 'card-123' as any } + + const result = await middlewares.findMessagesMeta(session, params) + + expect(mockHead.findMessagesMeta).toHaveBeenCalledWith(session, params) + expect(result).toEqual([{ messageId: 'msg-1' }]) + }) + + it('should return empty array if no head', async () => { + const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) + + const result = await middlewaresNoHead.findMessagesMeta(session, { cardId: 'card-123' as any }) + + expect(result).toEqual([]) + }) + }) + + describe('findMessagesGroups', () => { + it('should delegate to head middleware', async () => { + const params = { cardId: 'card-123' as any } + + const result = await middlewares.findMessagesGroups(session, params) + + expect(mockHead.findMessagesGroups).toHaveBeenCalledWith(session, params) + expect(result).toEqual([{ blobId: 'blob-1' }]) + }) + + it('should return empty array if no head', async () => { + const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) + + const result = await middlewaresNoHead.findMessagesGroups(session, { cardId: 'card-123' as any }) + + expect(result).toEqual([]) + }) + }) + + describe('findNotificationContexts', () => { + it('should delegate to head middleware', async () => { + const params = { cardId: 'card-123' as any } + const subscription = 'sub-123' + + const result = await middlewares.findNotificationContexts(session, params, subscription) + + expect(mockHead.findNotificationContexts).toHaveBeenCalledWith(session, params, subscription) + expect(result).toEqual([{ id: 'ctx-1' }]) + }) + + it('should return empty array if no head', async () => { + const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) + + const result = await middlewaresNoHead.findNotificationContexts(session, { cardId: 'card-123' as any }) + + expect(result).toEqual([]) + }) + }) + + describe('findNotifications', () => { + it('should delegate to head middleware', async () => { + const params = { contextId: 'ctx-123' as any } + const subscription = 'sub-123' + + const result = await middlewares.findNotifications(session, params, subscription) + + expect(mockHead.findNotifications).toHaveBeenCalledWith(session, params, subscription) + expect(result).toEqual([{ id: 'notif-1' }]) + }) + + it('should return empty array if no head', async () => { + const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) + + const result = await middlewaresNoHead.findNotifications(session, { contextId: 'ctx-123' as any }) + + expect(result).toEqual([]) + }) + }) + + describe('findLabels', () => { + it('should delegate to head middleware', async () => { + const params = { cardId: 'card-123' as any } + + const result = await middlewares.findLabels(session, params) + + expect(mockHead.findLabels).toHaveBeenCalledWith(session, params) + expect(result).toEqual([{ labelId: 'label-1' }]) + }) + + it('should return empty array if no head', async () => { + const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) + + const result = await middlewaresNoHead.findLabels(session, { cardId: 'card-123' as any }) + + expect(result).toEqual([]) + }) + }) + + describe('findCollaborators', () => { + it('should delegate to head middleware', async () => { + const params = { cardId: 'card-123' as any } + + const result = await middlewares.findCollaborators(session, params) + + expect(mockHead.findCollaborators).toHaveBeenCalledWith(session, params) + expect(result).toEqual([{ account: accountUuid }]) + }) + + it('should return empty array if no head', async () => { + const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) + + const result = await middlewaresNoHead.findCollaborators(session, { cardId: 'card-123' as any }) + + expect(result).toEqual([]) + }) + }) + + describe('findPeers', () => { + it('should delegate to head middleware', async () => { + const params = { workspaceId: workspace, cardId: 'card-123' as any } + + const result = await middlewares.findPeers(session, params) + + expect(mockHead.findPeers).toHaveBeenCalledWith(session, params) + expect(result).toEqual([{ cardId: 'card-1' }]) + }) + + it('should return empty array if no head', async () => { + const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) + + const result = await middlewaresNoHead.findPeers(session, { workspaceId: workspace, cardId: 'card-123' as any }) + + expect(result).toEqual([]) + }) + }) + + describe('event', () => { + it('should process event through middleware chain', async () => { + const event: any = { + type: MessageEventType.CreateMessage, + cardId: 'card-123', + messageId: 'msg-123', + cardType: 'task' as CardType, + messageType: 'text', + content: 'Test' as Markdown, + socialId, + date: new Date() + } + + const result = await middlewares.event(session, event) + + expect(mockHead.event).toHaveBeenCalledWith(session, event, false) + expect(mockHead.handleBroadcast).toHaveBeenCalledWith(session, [event]) + expect(result).toBeDefined() + }) + + it('should handle derived flag from session', async () => { + const event: any = { + type: MessageEventType.UpdatePatch, + cardId: 'card-123', + messageId: 'msg-123', + content: 'Updated' as Markdown, + socialId, + date: new Date() + } + + session.derived = true + + await middlewares.event(session, event) + + expect(mockHead.event).toHaveBeenCalledWith(session, event, true) + }) + + it('should return empty object if no head', async () => { + const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) + + const event: any = { + type: MessageEventType.CreateMessage, + date: new Date() + } + + const result = await middlewaresNoHead.event(session, event) + + expect(result).toEqual({}) + }) + }) + + describe('subscribeCard', () => { + it('should delegate to head middleware', () => { + const cardId = 'card-123' as any + const subscription = 'sub-123' + + middlewares.subscribeCard(session, cardId, subscription) + + expect(mockHead.subscribeCard).toHaveBeenCalledWith(session, cardId, subscription) + }) + + it('should do nothing if no head', () => { + const middlewaresNoHead = Middlewares.create(mockMeasureCtx, mockContext, []) + + expect(() => { + void middlewaresNoHead.then((m) => { + m.subscribeCard(session, 'card-123' as any, 'sub-123') + }) + }).not.toThrow() + }) + }) + + describe('unsubscribeCard', () => { + it('should delegate to head middleware', () => { + const cardId = 'card-123' as any + const subscription = 'sub-123' + + middlewares.unsubscribeCard(session, cardId, subscription) + + expect(mockHead.unsubscribeCard).toHaveBeenCalledWith(session, cardId, subscription) + }) + + it('should do nothing if no head', async () => { + const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) + + expect(() => { + middlewaresNoHead.unsubscribeCard(session, 'card-123' as any, 'sub-123') + }).not.toThrow() + }) + }) + + describe('closeSession', () => { + it('should delegate to head middleware', async () => { + await middlewares.closeSession('session-123') + + expect(mockHead.closeSession).toHaveBeenCalledWith('session-123') + }) + + it('should do nothing if no head', async () => { + const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) + + await expect(middlewaresNoHead.closeSession('session-123')).resolves.not.toThrow() + }) + }) + + describe('close', () => { + it('should close all middlewares', async () => { + await middlewares.close() + + expect(mockHead.close).toHaveBeenCalled() + }) + + it('should handle errors during close', async () => { + // Create a fresh context to avoid mock clearing issues + const freshMockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis(), + contextData: undefined + } as unknown as jest.Mocked + + const freshContext = { + ...mockContext, + ctx: freshMockMeasureCtx + } + + // Create a new middlewares instance with a close function that throws + const mockMiddleware = { + event: jest.fn().mockResolvedValue({}), + findMessagesMeta: jest.fn().mockResolvedValue([]), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + handleBroadcast: jest.fn(), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + close: jest.fn(() => { + throw new Error('Close error') + }), + closeSession: jest.fn() + } + + const createFns: MiddlewareCreateFn[] = [async () => mockMiddleware] + + const testMiddlewares = await Middlewares.create(freshMockMeasureCtx, freshContext, createFns) + + await testMiddlewares.close() + + expect(mockMiddleware.close).toHaveBeenCalled() + expect(freshMockMeasureCtx.error).toHaveBeenCalledWith( + 'Failed to close middleware', + expect.objectContaining({ + workspace + }) + ) + }) + + it('should do nothing if no head', async () => { + const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) + + await expect(middlewaresNoHead.close()).resolves.not.toThrow() + }) + }) + }) + + describe('Edge cases', () => { + it('should handle empty middleware chain', async () => { + const middlewares = await Middlewares.create(mockMeasureCtx, mockContext, []) + + const event: any = { + type: MessageEventType.CreateMessage, + date: new Date() + } + + const result = await middlewares.event(session, event) + expect(result).toEqual({}) + + const messages = await middlewares.findMessagesMeta(session, { cardId: 'card-123' as any }) + expect(messages).toEqual([]) + }) + + it('should handle multiple middlewares in chain', async () => { + const middleware1Called: string[] = [] + const middleware2Called: string[] = [] + + const createFns: MiddlewareCreateFn[] = [ + async (context, next) => + ({ + ...next, + event: jest.fn().mockImplementation(async (s, e, d) => { + middleware1Called.push('event') + return next?.event != null ? await next.event(s, e, d) : {} + }), + findMessagesMeta: jest.fn().mockImplementation(async (s, p) => { + middleware1Called.push('findMessagesMeta') + return next?.findMessagesMeta != null ? await next.findMessagesMeta(s, p) : [] + }), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + handleBroadcast: jest.fn(), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + close: jest.fn(), + closeSession: jest.fn() + }) as any, + async (context, next) => + ({ + ...next, + event: jest.fn().mockImplementation(async (s, e, d) => { + middleware2Called.push('event') + return {} + }), + findMessagesMeta: jest.fn().mockImplementation(async () => { + middleware2Called.push('findMessagesMeta') + return [{ messageId: 'msg-1' }] + }), + findMessagesGroups: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + findNotifications: jest.fn().mockResolvedValue([]), + findLabels: jest.fn().mockResolvedValue([]), + findCollaborators: jest.fn().mockResolvedValue([]), + findPeers: jest.fn().mockResolvedValue([]), + handleBroadcast: jest.fn(), + subscribeCard: jest.fn(), + unsubscribeCard: jest.fn(), + close: jest.fn(), + closeSession: jest.fn() + }) as any + ] + + const middlewares = await Middlewares.create(mockMeasureCtx, mockContext, createFns) + + await middlewares.event(session, { type: MessageEventType.CreateMessage, date: new Date() } as any) + await middlewares.findMessagesMeta(session, { cardId: 'card-123' as any }) + + expect(middleware1Called).toContain('event') + expect(middleware1Called).toContain('findMessagesMeta') + expect(middleware2Called).toContain('event') + expect(middleware2Called).toContain('findMessagesMeta') + }) + }) +}) diff --git a/foundations/communication/packages/server/src/__tests__/notification/notification.test.ts b/foundations/communication/packages/server/src/__tests__/notification/notification.test.ts new file mode 100644 index 0000000000..9431cb808a --- /dev/null +++ b/foundations/communication/packages/server/src/__tests__/notification/notification.test.ts @@ -0,0 +1,872 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { type MeasureContext, type WorkspaceUuid, readOnlyGuestAccountUuid } from '@hcengineering/core' +import { + type Event, + MessageEventType, + NotificationEventType, + type CreateNotificationContextResult +} from '@hcengineering/communication-sdk-types' +import { + type AccountUuid, + type BlobID, + type CardID, + type CardType, + type ContextID, + type Markdown, + type MessageID, + type MessageMeta, + type NotificationContext, + NotificationType, + type ReactionNotificationContent, + type SocialID +} from '@hcengineering/communication-types' + +import { notify } from '../../notification/notification' +import { type TriggerCtx, type Enriched } from '../../types' +import { getNameBySocialID } from '../../triggers/utils' + +// Mock dependencies +jest.mock('@hcengineering/text-markdown', () => ({ + markdownToMarkup: jest.fn((md) => ({ type: 'doc', content: [{ type: 'text', text: md }] })) +})) + +jest.mock('@hcengineering/text-core', () => ({ + jsonToMarkup: jest.fn((json) => json), + markupToText: jest.fn((markup) => { + if (typeof markup === 'string') return markup + if (markup?.content?.[0]?.text !== undefined) return markup.content[0].text + return 'Test message text' + }) +})) + +jest.mock('../../triggers/utils', () => ({ + getNameBySocialID: jest.fn().mockResolvedValue('John Doe') +})) + +describe('notification', () => { + let mockCtx: TriggerCtx + let mockClient: any + let mockMeasureCtx: jest.Mocked + + const workspace = 'test-workspace' as WorkspaceUuid + const cardId = 'card-123' as CardID + const messageId = 'message-123' as MessageID + const blobId = 'blob-123' as BlobID + const socialId = 'social-123' as SocialID + const accountUuid = 'account-123' as AccountUuid + const contextId = 'context-123' as ContextID + const cardType = 'card' as CardType + + beforeEach(() => { + jest.clearAllMocks() + + // Reset the getNameBySocialID mock to its default behavior + const mockGetName = getNameBySocialID as jest.MockedFunction + mockGetName.mockResolvedValue('John Doe') + + mockMeasureCtx = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + newChild: jest.fn().mockReturnThis() + } as any + + mockClient = { + db: { + findNotifications: jest.fn().mockResolvedValue([]), + findNotificationContexts: jest.fn().mockResolvedValue([]), + getCardSpaceMembers: jest.fn().mockResolvedValue([accountUuid]), + getCollaboratorsCursor: jest.fn(), + getCardTitle: jest.fn().mockResolvedValue('Test Card'), + getNameByAccount: jest.fn().mockResolvedValue('John Doe') + }, + getMessageMeta: jest.fn(), + findPersonUuid: jest.fn() + } + + mockCtx = { + ctx: mockMeasureCtx, + client: mockClient, + workspace, + metadata: { + accountsUrl: 'http://accounts', + hulylakeUrl: 'http://hulylake', + secret: 'secret', + messagesPerBlob: 200 + }, + account: { + uuid: accountUuid, + socialIds: [socialId] + } as any, + execute: jest.fn() + } as any + }) + + describe('notify', () => { + describe('CreateMessage event', () => { + it('should return empty array when noNotify option is true', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId, + messageId, + content: 'Hello' as Markdown, + socialId, + date: new Date(), + cardType, + options: { noNotify: true } + } as any + + const result = await notify(mockCtx, event) + + expect(result).toEqual([]) + const getMessageMetaSpy = mockClient.getMessageMeta as jest.Mock + expect(getMessageMetaSpy).toHaveBeenCalledTimes(0) + }) + + it('should return empty array when messageId is null', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId, + messageId: null, + content: 'Hello' as Markdown, + socialId, + date: new Date(), + cardType + } as any + + const result = await notify(mockCtx, event) + + expect(result).toEqual([]) + const getMessageMetaSpy = mockClient.getMessageMeta as jest.Mock + expect(getMessageMetaSpy).toHaveBeenCalledTimes(0) + }) + + it('should return empty array when message meta is not found', async () => { + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId, + messageId, + content: 'Hello' as Markdown, + socialId, + date: new Date(), + cardType + } as any + + mockClient.getMessageMeta.mockResolvedValue(undefined) + + const result = await notify(mockCtx, event) + + expect(result).toEqual([]) + const getMessageMetaSpy = mockClient.getMessageMeta as jest.Mock + expect(getMessageMetaSpy).toHaveBeenCalledTimes(1) + expect(getMessageMetaSpy).toHaveBeenLastCalledWith(cardId, messageId) + }) + + it('should create notification for message', async () => { + const date = new Date() + const creatorSocialId = 'creator-social' as SocialID + const creatorAccount = 'creator-account' as AccountUuid + const collaboratorAccount = 'collaborator-account' as AccountUuid + + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId, + messageId, + content: 'Hello World' as Markdown, + socialId: creatorSocialId, + date, + cardType + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: creatorSocialId, + createdOn: date.getTime() + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + + // First call for message creator, second for collaborator + mockClient.findPersonUuid + .mockResolvedValueOnce(creatorAccount) // creator account + .mockResolvedValueOnce(creatorAccount) // when checking if it's own message + + mockClient.db.getCardSpaceMembers.mockResolvedValue([collaboratorAccount, creatorAccount]) + + // Mock collaborators cursor - return a collaborator that's not the creator + const collaborators = [{ account: collaboratorAccount, personUuid: 'person-1' as any }] + mockClient.db.getCollaboratorsCursor.mockReturnValue({ + [Symbol.asyncIterator]: async function * () { + yield collaborators + } + }) + + // Mock context doesn't exist, will be created + mockClient.db.findNotificationContexts.mockResolvedValue([]) + + // Mock context creation + const contextResult: CreateNotificationContextResult = { id: contextId } + mockCtx.execute = jest.fn().mockResolvedValue(contextResult) + + const result = await notify(mockCtx, event) + + expect(result.length).toBeGreaterThan(0) + const getMessageMetaSpy = mockClient.getMessageMeta as jest.Mock + expect(getMessageMetaSpy).toHaveBeenCalledTimes(1) + expect(getMessageMetaSpy).toHaveBeenLastCalledWith(cardId, messageId) + }) + + it('should skip collaborators not in space members', async () => { + const date = new Date() + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId, + messageId, + content: 'Hello' as Markdown, + socialId, + date, + cardType + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: date.getTime() + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid.mockResolvedValue('other-account' as AccountUuid) + mockClient.db.getCardSpaceMembers.mockResolvedValue([accountUuid]) + + const collaborators = [{ account: 'other-account' as AccountUuid, personUuid: 'person-2' as any }] + mockClient.db.getCollaboratorsCursor.mockReturnValue({ + [Symbol.asyncIterator]: async function * () { + yield collaborators + } + }) + + const result = await notify(mockCtx, event) + + expect(result).toEqual([]) + }) + + it('should handle errors during collaborator processing', async () => { + const date = new Date() + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId, + messageId, + content: 'Hello' as Markdown, + socialId, + date, + cardType + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: date.getTime() + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + + // Make sure the creator account is different from collaborators + mockClient.findPersonUuid + .mockResolvedValueOnce('creator-account' as AccountUuid) // creator account (for socialId) + .mockResolvedValueOnce('creator-account' as AccountUuid) // used for isOwn check for first collaborator + .mockResolvedValueOnce('creator-account' as AccountUuid) // used for isOwn check for second collaborator + + // Two collaborators - processing will fail for the second one + const collaborators = [ + { account: accountUuid, personUuid: 'person-1' as any }, + { account: 'account-2' as AccountUuid, personUuid: 'person-2' as any } + ] + mockClient.db.getCollaboratorsCursor.mockReturnValue({ + [Symbol.asyncIterator]: async function * () { + yield collaborators + } + }) + + mockClient.db.getCardSpaceMembers.mockResolvedValue([accountUuid, 'account-2' as AccountUuid]) + + // Return contexts for BOTH collaborators so neither needs creation + const firstContext: NotificationContext = { + id: contextId, + cardId, + account: accountUuid, + lastUpdate: date, + lastView: date, + lastNotify: date + } as any + + const secondContext: NotificationContext = { + id: 'context-456' as ContextID, + cardId, + account: 'account-2' as AccountUuid, + lastUpdate: date, + lastView: date, + lastNotify: date + } as any + + mockClient.db.findNotificationContexts.mockResolvedValue([firstContext, secondContext]) + + // Make getNameBySocialID throw an error on the second call + const mockGetName = getNameBySocialID as jest.MockedFunction + mockGetName + .mockResolvedValueOnce('John Doe') // First collaborator succeeds + .mockRejectedValueOnce(new Error('Database error')) // Second collaborator fails + + const result = await notify(mockCtx, event) + + // Should have logged the error + expect(mockMeasureCtx.error).toHaveBeenCalledWith( + 'Error on create notification', + expect.objectContaining({ + collaborator: 'account-2', + error: expect.any(Error) + }) + ) + // Result should contain events from first collaborator even though second failed + expect(result).toBeDefined() + expect(Array.isArray(result)).toBe(true) + }) + + it('should continue processing other collaborators when one fails', async () => { + const date = new Date() + const event: Enriched = { + type: MessageEventType.CreateMessage, + cardId, + messageId, + content: 'Hello' as Markdown, + socialId, + date, + cardType + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: date.getTime() + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid.mockResolvedValue(accountUuid) + + const collaborators = [ + { account: accountUuid, personUuid: 'person-1' as any } + ] + mockClient.db.getCollaboratorsCursor.mockReturnValue({ + [Symbol.asyncIterator]: async function * () { + yield collaborators + } + }) + + const context: NotificationContext = { + id: contextId, + cardId, + account: accountUuid, + lastUpdate: date, + lastView: date, + lastNotify: date + } as any + + mockClient.db.findNotificationContexts.mockResolvedValue([context]) + + const result = await notify(mockCtx, event) + + // Should return successfully even if there are potential errors + expect(result).toBeDefined() + expect(Array.isArray(result)).toBe(true) + }) + }) + + describe('ReactionPatch event - add', () => { + it('should create notification for reaction', async () => { + const date = new Date() + const reactionSocialId = 'reaction-social' as SocialID + const messageSocialId = 'message-social' as SocialID + const messageAccount = 'message-account' as AccountUuid + const reactionAccount = 'reaction-account' as AccountUuid + + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId, + messageId, + operation: { + opcode: 'add', + reaction: '👍' + }, + socialId: reactionSocialId, + date + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: messageSocialId, + createdOn: date.getTime() + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid + .mockResolvedValueOnce(messageAccount) // message creator + .mockResolvedValueOnce(reactionAccount) // reaction creator + + mockClient.db.getCardSpaceMembers.mockResolvedValue([messageAccount, reactionAccount]) + + const context: NotificationContext = { + id: contextId, + cardId, + account: messageAccount, + lastUpdate: new Date(date.getTime() - 1000), + lastView: new Date(date.getTime() - 2000), + lastNotify: new Date(date.getTime() - 1000) + } as any + + mockClient.db.findNotificationContexts.mockResolvedValue([context]) + + const result = await notify(mockCtx, event) + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: NotificationEventType.CreateNotification, + notificationType: NotificationType.Reaction, + messageId + }) + ]) + ) + }) + + it('should return empty array when message meta is not found', async () => { + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId, + messageId, + operation: { opcode: 'add', reaction: '👍' }, + socialId, + date: new Date() + } as any + + mockClient.getMessageMeta.mockResolvedValue(undefined) + + const result = await notify(mockCtx, event) + + expect(result).toEqual([]) + }) + + it('should return empty array when message account is not found', async () => { + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId, + messageId, + operation: { opcode: 'add', reaction: '👍' }, + socialId, + date: new Date() + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: Date.now() + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid.mockResolvedValue(undefined) + + const result = await notify(mockCtx, event) + + expect(result).toEqual([]) + }) + + it('should return empty array when message account is not in space members', async () => { + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId, + messageId, + operation: { opcode: 'add', reaction: '👍' }, + socialId, + date: new Date() + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: Date.now() + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid.mockResolvedValue('other-account' as AccountUuid) + mockClient.db.getCardSpaceMembers.mockResolvedValue([accountUuid]) + + const result = await notify(mockCtx, event) + + expect(result).toEqual([]) + }) + + it('should not notify when reacting to own message', async () => { + const date = new Date() + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId, + messageId, + operation: { opcode: 'add', reaction: '👍' }, + socialId, + date + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: date.getTime() + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid.mockResolvedValue(accountUuid) + + const result = await notify(mockCtx, event) + + expect(result).toEqual([]) + }) + + it('should create context if it does not exist', async () => { + const date = new Date() + const otherSocialId = 'other-social' as SocialID + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId, + messageId, + operation: { opcode: 'add', reaction: '👍' }, + socialId: otherSocialId, + date + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: date.getTime() + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid + .mockResolvedValueOnce(accountUuid) // message creator + .mockResolvedValueOnce('other-account' as AccountUuid) // reaction creator + + mockClient.db.findNotificationContexts.mockResolvedValue([]) + + const createContextResult: CreateNotificationContextResult = { id: contextId } + mockCtx.execute = jest.fn().mockResolvedValue(createContextResult) + + const result = await notify(mockCtx, event) + + expect(mockCtx.execute).toHaveBeenCalledWith( + expect.objectContaining({ + type: NotificationEventType.CreateNotificationContext + }) + ) + expect(result.length).toBeGreaterThan(0) + }) + + it('should update context lastNotify if reaction is newer', async () => { + const date = new Date() + const oldDate = new Date(date.getTime() - 10000) + const otherSocialId = 'other-social' as SocialID + + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId, + messageId, + operation: { opcode: 'add', reaction: '👍' }, + socialId: otherSocialId, + date + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: oldDate.getTime() + } as any + + const context: NotificationContext = { + id: contextId, + cardId, + account: accountUuid, + lastUpdate: oldDate, + lastView: oldDate, + lastNotify: oldDate + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid + .mockResolvedValueOnce(accountUuid) + .mockResolvedValueOnce('other-account' as AccountUuid) + mockClient.db.findNotificationContexts.mockResolvedValue([context]) + + const result = await notify(mockCtx, event) + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: NotificationEventType.UpdateNotificationContext, + updates: expect.objectContaining({ + lastNotify: date + }) + }) + ]) + ) + }) + + it('should mark notification as read for readOnlyGuestAccount', async () => { + const date = new Date() + const otherSocialId = 'other-social' as SocialID + + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId, + messageId, + operation: { opcode: 'add', reaction: '👍' }, + socialId: otherSocialId, + date + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: date.getTime() + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid + .mockResolvedValueOnce(readOnlyGuestAccountUuid) // message creator + .mockResolvedValueOnce('other-account' as AccountUuid) // reaction creator + + // Make sure readOnlyGuestAccount is in space members + mockClient.db.getCardSpaceMembers.mockResolvedValue([readOnlyGuestAccountUuid, 'other-account' as AccountUuid]) + mockClient.db.findNotificationContexts.mockResolvedValue([]) + mockCtx.execute = jest.fn().mockResolvedValue({ id: contextId }) + + const result = await notify(mockCtx, event) + + const createNotification = result.find( + (e) => e.type === NotificationEventType.CreateNotification + ) + expect(createNotification).toBeDefined() + expect(createNotification).toMatchObject({ read: true }) + }) + }) + + describe('ReactionPatch event - remove', () => { + it('should remove reaction notification', async () => { + const date = new Date() + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId, + messageId, + operation: { opcode: 'remove', reaction: '👍' }, + socialId, + date + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: date.getTime() + } as any + + const notificationContent: ReactionNotificationContent = { + emoji: '👍', + title: 'Reacted to your message', + shortText: '👍', + senderName: 'John Doe' + } + + const notification = { + id: 'notif-1', + contextId, + type: NotificationType.Reaction, + messageId, + account: accountUuid, + created: date, + content: notificationContent, + creator: socialId + } + + const context: NotificationContext = { + id: contextId, + cardId, + account: accountUuid, + lastUpdate: date, + lastView: date, + lastNotify: date + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid.mockResolvedValue(accountUuid) + mockClient.db.findNotifications.mockResolvedValue([notification as any]) + mockClient.db.findNotificationContexts.mockResolvedValue([context]) + + const result = await notify(mockCtx, event) + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: NotificationEventType.RemoveNotifications, + ids: ['notif-1'] + }) + ]) + ) + }) + + it('should return empty array when notification not found', async () => { + const date = new Date() + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId, + messageId, + operation: { opcode: 'remove', reaction: '👍' }, + socialId, + date + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: date.getTime() + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid.mockResolvedValue(accountUuid) + mockClient.db.findNotifications.mockResolvedValue([]) + + const result = await notify(mockCtx, event) + + expect(result).toEqual([]) + }) + + it('should update context lastNotify when removed notification was the last one', async () => { + const date = new Date() + const olderDate = new Date(date.getTime() - 5000) + + const event: Enriched = { + type: MessageEventType.ReactionPatch, + cardId, + messageId, + operation: { opcode: 'remove', reaction: '👍' }, + socialId, + date + } as any + + const meta: MessageMeta = { + cardId, + id: messageId, + blobId, + creator: socialId, + createdOn: date.getTime() + } as any + + const notificationContent: ReactionNotificationContent = { + emoji: '👍', + title: 'Reacted to your message', + shortText: '👍', + senderName: 'John Doe' + } + + const notification = { + id: 'notif-1', + contextId, + type: NotificationType.Reaction, + messageId, + account: accountUuid, + created: date, + content: notificationContent, + creator: socialId + } + + const olderNotification = { + id: 'notif-2', + contextId, + created: olderDate + } + + const context: NotificationContext = { + id: contextId, + cardId, + account: accountUuid, + lastUpdate: date, + lastView: olderDate, + lastNotify: date + } as any + + mockClient.getMessageMeta.mockResolvedValue(meta) + mockClient.findPersonUuid.mockResolvedValue(accountUuid) + mockClient.db.findNotifications + .mockResolvedValueOnce([notification as any]) + .mockResolvedValueOnce([olderNotification as any]) + mockClient.db.findNotificationContexts.mockResolvedValue([context]) + + const result = await notify(mockCtx, event) + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: NotificationEventType.UpdateNotificationContext, + updates: expect.objectContaining({ + lastNotify: olderDate + }) + }) + ]) + ) + }) + }) + + describe('Unknown event type', () => { + it('should return empty array for unknown event type', async () => { + const event: Enriched = { + type: 'unknown.event' as any, + date: new Date() + } as any + + const result = await notify(mockCtx, event) + + expect(result).toEqual([]) + }) + }) + }) +}) diff --git a/foundations/communication/packages/server/src/blob.ts b/foundations/communication/packages/server/src/blob.ts new file mode 100644 index 0000000000..181bf7ce85 --- /dev/null +++ b/foundations/communication/packages/server/src/blob.ts @@ -0,0 +1,594 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + MeasureContext, + PersonUuid, + SortingOrder, + systemAccountUuid, + WorkspaceUuid +} from '@hcengineering/core' +import { getWorkspaceClient, type HulylakeWorkspaceClient, type JsonPatch } from '@hcengineering/hulylake-client' +import { generateToken } from '@hcengineering/server-token' +import { + Attachment, + AttachmentID, + AttachmentUpdateData, + BlobID, + CardID, + CardType, + FindMessagesGroupParams, + Markdown, + Message, + MessageDoc, + MessageExtra, + MessageID, + MessagesDoc, + MessagesGroup, + MessagesGroupDoc, + MessagesGroupsDoc, + Thread + , ComparisonOperator +} from '@hcengineering/communication-types' +import { v4 as uuid } from 'uuid' + +import { Metadata } from './types' + +const LOG_CARD_ID = '67ecc702f182d88819f0a726' as CardID + +export class Blob { + private readonly client: HulylakeWorkspaceClient + // Groups sored by fromDate + private readonly messageGroupsByCardId = new Map() + private readonly messageGroupsPromises = new Map>() + + private readonly messageGroupCreationPromises = new Map>() + + private readonly retryOptions = { + maxRetries: 3, + isRetryable: () => true, + delayStrategy: { + getDelay: () => 1000 + } + } as const + + constructor (private readonly ctx: MeasureContext, private readonly workspace: WorkspaceUuid, private readonly metadata: Metadata) { + this.client = getWorkspaceClient(metadata.hulylakeUrl, workspace, generateToken(systemAccountUuid, workspace, undefined, metadata.secret)) + } + + public async findMessagesGroups (params: FindMessagesGroupParams): Promise { + const { cardId, fromDate, toDate, blobId, limit, order } = params + const groups = await this.getAllMessageGroups(cardId) + + if (order === SortingOrder.Ascending) { + groups.sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime()) + } else if (order === SortingOrder.Descending) { + groups.sort((a, b) => b.fromDate.getTime() - a.fromDate.getTime()) + } + + if (fromDate == null && toDate == null && blobId == null && limit == null) { + return groups + } + + const result: MessagesGroup[] = [] + for (const group of groups) { + if (blobId != null && group.blobId !== blobId) continue + if (fromDate != null && !matchDate(group.fromDate, fromDate)) continue + if (toDate != null && !matchDate(group.toDate, toDate)) continue + + result.push(group) + + if (limit != null && result.length >= limit) break + } + + return result + } + + private async getAllMessageGroups (cardId: CardID): Promise { + const createPromise = this.messageGroupCreationPromises.get(cardId) + + if (createPromise != null) { + await createPromise + } + + if (this.messageGroupsByCardId.has(cardId)) { + return (this.messageGroupsByCardId.get(cardId) ?? []).sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime()) + } + + const existingPromise = this.messageGroupsPromises.get(cardId) + if (existingPromise != null) return await existingPromise + + const promise = (async () => { + try { + const res = await this.client.getJson(`${cardId}/messages/groups`, this.retryOptions) + if (res.status === 404) { + await this.createMessagesGroupBlob(cardId) + this.messageGroupsByCardId.set(cardId, []) + return [] + } + + if (cardId === LOG_CARD_ID) { + this.ctx.info('Received groups', { groups: JSON.stringify(res.body ?? {}, undefined, 2) }) + } + + const groups = Object.values(res.body ?? {}).map(it => this.deserializeMessageGroup(it)).sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime()) + this.messageGroupsByCardId.set(cardId, groups) + return groups + } finally { + this.messageGroupsPromises.delete(cardId) + } + })() + + this.messageGroupsPromises.set(cardId, promise) + return await promise + } + + public async getMessageGroupByDate (cardId: CardID, date: Date, create = true): Promise { + const all = await this.getAllMessageGroups(cardId) + if (cardId === LOG_CARD_ID) { + this.ctx.info('all groups sorted', { cardId, sortedGroups: JSON.stringify(all, undefined, 2) }) + } + const ts = date.getTime() + + const match = all.find(g => g.fromDate.getTime() <= ts && g.toDate.getTime() >= ts) + if (match != null) { + if (cardId === LOG_CARD_ID) { + this.ctx.info('math group', { date, match: JSON.stringify(match, undefined, 2) }) + } + return match + } + + const lastGroup = all[all.length - 1] + if (lastGroup != null && lastGroup.fromDate.getTime() <= ts && lastGroup.count < this.metadata.messagesPerBlob) { + if (cardId === LOG_CARD_ID) { + this.ctx.info('last group', { date, match: JSON.stringify(match, undefined, 2) }) + } + return lastGroup + } + + const firstGroup = all[0] + if (firstGroup != null && firstGroup.fromDate.getTime() >= ts && firstGroup.count < this.metadata.messagesPerBlob) { + if (cardId === LOG_CARD_ID) { + this.ctx.info('first group', { date, match: JSON.stringify(match, undefined, 2) }) + } + return firstGroup + } + + if (create) return await this.createMessageGroup(cardId, date) + + return undefined + } + + private async createMessagesGroupBlob (cardId: CardID): Promise { + await this.client.putJson(`${cardId}/messages/groups`, {}, undefined, this.retryOptions) + } + + private async incrementMessagesCount (cardId: CardID, blobId: BlobID, toDate?: Date, fromDate?: Date): Promise { + const groups = await this.getAllMessageGroups(cardId) + const group = groups.find((g) => g.blobId === blobId) + + if (group == null) return + + this.messageGroupsByCardId.set(cardId, groups.map((g) => g.blobId === blobId ? ({ ...g, count: g.count + 1, toDate: toDate ?? group.toDate, fromDate: fromDate ?? group.fromDate }) : g)) + + const patches: JsonPatch[] = [ + { + hop: 'inc', + path: `/${blobId}/count`, + value: 1 + }, + ...toDate != null + ? [{ + op: 'replace', + path: `/${blobId}/toDate`, + value: toDate + } as const] + : [], + ...fromDate != null + ? [{ + hop: 'add', + path: `/${blobId}/fromDate`, + value: fromDate + } as const] + : [] + ] + await this.client.patchJson(`${cardId}/messages/groups`, patches, undefined, this.retryOptions) + } + + private async decrementMessagesCount (cardId: CardID, blobId: BlobID): Promise { + const groups = await this.getAllMessageGroups(cardId) + const group = groups.find((g) => g.blobId === blobId) + + if (group == null) return + + const count = group.count - 1 + group.count = count + this.messageGroupsByCardId.set(cardId, groups.map((g) => g.blobId === blobId ? ({ ...g, count }) : g)) + + const patches: JsonPatch[] = [ + { + hop: 'inc', + path: `/${blobId}/count`, + value: -1 + } + ] + await this.client.patchJson(`${cardId}/messages/groups`, patches, undefined, this.retryOptions) + } + + private async createMessageGroup (cardId: CardID, date: Date): Promise { + const createPromise = this.messageGroupCreationPromises.get(cardId) + + if (createPromise != null) { + await createPromise + const group = await this.getMessageGroupByDate(cardId, date, false) + if (group != null) return group + } + + const promise = (async () => { + try { + const groupDoc: MessagesGroupDoc = { + cardId, + blobId: uuid() as BlobID, + fromDate: date.toISOString(), + toDate: date.toISOString(), + count: 0 + } + const patches: JsonPatch[] = [ + { + hop: 'add', + path: `/${groupDoc.blobId}`, + value: groupDoc, + safe: true + } + ] + + await this.client.patchJson(`${cardId}/messages/groups`, patches, undefined, this.retryOptions) + const group = this.deserializeMessageGroup(groupDoc) + if (this.messageGroupsByCardId.has(cardId)) { + this.messageGroupsByCardId.set(cardId, + [...this.messageGroupsByCardId.get(cardId) ?? [], group].sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime()) + ) + } else { + this.messageGroupsByCardId.set(cardId, [group]) + } + await this.createMessagesBlob(cardId, groupDoc.blobId, date, date) + + return group + } finally { + this.messageGroupCreationPromises.delete(cardId) + } + })() + + this.messageGroupCreationPromises.set(cardId, promise) + return await promise + } + + private async createMessagesBlob (cardId: CardID, blobId: BlobID, from: Date, to: Date): Promise { + const initialJson: MessagesDoc = { + cardId, + fromDate: from.toISOString(), + toDate: to.toISOString(), + language: 'original', + messages: {} + } + + await this.client.putJson(`${cardId}/messages/${blobId}`, initialJson, undefined, this.retryOptions) + } + + async insertMessage (cardId: CardID, group: MessagesGroup, message: Message): Promise { + if (cardId === LOG_CARD_ID && group.blobId === 'ad77d5d3-a073-4a14-960b-2f46e844bb6d"') { + this.ctx.error('SELECT WRONG GROUP!', { cardId, group, message, groups: this.messageGroupsByCardId.get(cardId) }) + throw new Error('Select wrong group') + } + const updateToDate = message.created.getTime() > group.toDate.getTime() + const updateFromDate = message.created.getTime() < group.fromDate.getTime() + + const serializedMessage = this.serializeMessage(message) + const patches: JsonPatch[] = [ + { + hop: 'add', + path: `/messages/${message.id}`, + value: serializedMessage, + safe: true + }, + ...(updateToDate + ? [ + { + op: 'replace', + path: '/toDate', + value: message.created + } as const + ] + : []), + ...(updateFromDate + ? [ + { + hop: 'add', + path: '/fromDate', + value: message.created + } as const + ] + : []) + ] + await this.patchJson(cardId, group.blobId, patches) + void this.incrementMessagesCount(cardId, group.blobId, updateToDate ? message.created : undefined, updateFromDate ? message.created : undefined) + } + + async updateMessage (cardId: CardID, blobId: BlobID, messageId: MessageID, update: { + language?: string + content?: Markdown + extra?: MessageExtra + }, date: Date): Promise { + const patches: JsonPatch[] = [] + + if (update.content != null) { + patches.push({ + op: 'replace', + path: `/messages/${messageId}/content`, + value: update.content + }) + } + + if (update.extra != null) { + patches.push({ + op: 'replace', + path: `/messages/${messageId}/extra`, + value: update.extra + }) + } + + if (update.language != null) { + patches.push({ + op: 'replace', + path: `/messages/${messageId}/language`, + value: update.language + }) + } + + if (patches.length === 0) return + + if (update.content != null || update.extra != null) { + patches.push({ + op: 'replace', + path: `/messages/${messageId}/modified`, + value: date + }) + } + + await this.patchJson(cardId, blobId, patches) + } + + async removeMessage (cardId: CardID, blobId: BlobID, messageId: MessageID): Promise { + const patches: JsonPatch[] = [ + { + hop: 'remove', + path: `/messages/${messageId}`, + safe: true + } as const + ] + + await this.patchJson(cardId, blobId, patches) + void this.decrementMessagesCount(cardId, blobId) + } + + async addReaction (cardId: CardID, blobId: BlobID, messageId: MessageID, emoji: string, person: PersonUuid, date: Date): Promise { + const patches: JsonPatch[] = [ + { + hop: 'add', + path: `/messages/${messageId}/reactions/${emoji}`, + value: {}, + safe: true + }, + { + hop: 'add', + path: `/messages/${messageId}/reactions/${emoji}/${person}`, + value: { + count: 1, + date + }, + safe: true + } + ] + await this.patchJson(cardId, blobId, patches) + } + + async removeReaction (cardId: CardID, blobId: BlobID, messageId: MessageID, emoji: string, person: PersonUuid): Promise { + const patches: JsonPatch[] = [ + { + hop: 'remove', + path: `/messages/${messageId}/reactions/${emoji}/${person}`, + safe: true + } + ] + await this.patchJson(cardId, blobId, patches) + } + + async addAttachments (cardId: CardID, blobId: BlobID, messageId: MessageID, attachments: Attachment[]): Promise { + const patches: JsonPatch[] = [] + + for (const attachment of attachments) { + patches.push({ + op: 'add', + path: `/messages/${messageId}/attachments/${attachment.id}`, + value: attachment + }) + } + await this.patchJson(cardId, blobId, patches) + } + + async removeAttachments (cardId: CardID, blobId: BlobID, messageId: MessageID, attachmentIds: AttachmentID[]): Promise { + const patches: JsonPatch[] = [] + + for (const attachmentId of attachmentIds) { + patches.push({ + hop: 'remove', + path: `/messages/${messageId}/attachments/${attachmentId}`, + safe: true + }) + } + await this.patchJson(cardId, blobId, patches) + } + + async setAttachments (cardId: CardID, blobId: BlobID, messageId: MessageID, attachments: Attachment[]): Promise { + const patches: JsonPatch[] = [{ + op: 'replace', + path: `/messages/${messageId}/attachments`, + value: {} + }] + + for (const attachment of attachments) { + patches.push({ + op: 'add', + path: `/messages/${messageId}/attachments/${attachment.id}`, + value: attachment + }) + } + await this.patchJson(cardId, blobId, patches) + } + + async updateAttachments (cardId: CardID, blobId: BlobID, messageId: MessageID, updates: AttachmentUpdateData[], date: Date): Promise { + const patches: JsonPatch[] = [] + for (const update of updates) { + const keys = Object.keys(update.params) + if (keys.length === 0) continue + for (const key of keys) { + patches.push({ + op: 'add', + path: `/messages/${messageId}/attachments/${update.id}/params/${key}`, + value: update.params[key] + }) + } + patches.push({ + op: 'add', + path: `/messages/${messageId}/attachments/${update.id}/modified`, + value: date.toISOString() + }) + } + + await this.patchJson(cardId, blobId, patches) + } + + async attachThread (cardId: CardID, blobId: BlobID, messageId: MessageID, thread: Thread): Promise { + const patches: JsonPatch[] = [ + { + op: 'add', + path: `/messages/${messageId}/threads/${thread.threadId}`, + value: thread + } + ] + await this.patchJson(cardId, blobId, patches) + } + + async updateThread (cardId: CardID, blobId: BlobID, messageId: MessageID, threadId: CardID, update: { threadType: CardType }): Promise { + const patches: JsonPatch[] = [ + { + op: 'add', + path: `/messages/${messageId}/threads/${threadId}/threadType`, + value: update.threadType + } + ] + await this.patchJson(cardId, blobId, patches) + } + + async addThreadReply (cardId: CardID, blobId: BlobID, messageId: MessageID, threadId: CardID, person: PersonUuid, date: Date): Promise { + const patches: JsonPatch[] = + [ + { + hop: 'inc', + path: `/messages/${messageId}/threads/${threadId}/repliesCount`, + value: 1 + }, + { + op: 'add', + path: `/messages/${messageId}/threads/${threadId}/lastReply`, + value: date + }, + { + hop: 'inc', + path: `/messages/${messageId}/threads/${threadId}/repliedPersons/${person}`, + value: 1 + } + ] + + await this.patchJson(cardId, blobId, patches) + } + + async removeThreadReply (cardId: CardID, blobId: BlobID, messageId: MessageID, threadId: CardID, person: PersonUuid): Promise { + const patches: JsonPatch[] = + [ + { + hop: 'inc', + path: `/messages/${messageId}/threads/${threadId}/repliesCount`, + value: -1 + }, + { + hop: 'inc', + path: `/messages/${messageId}/threads/${threadId}/repliedPersons/${person}`, + value: -1 + } + ] + + await this.patchJson(cardId, blobId, patches) + } + + async removeThread (cardId: CardID, blobId: BlobID, messageId: MessageID, threadId: CardID): Promise { + const patches: JsonPatch[] = [ + { + hop: 'remove', + path: `/messages/${messageId}/threads/${threadId}`, + safe: true + } + ] + await this.patchJson(cardId, blobId, patches) + } + + private async patchJson (cardId: CardID, blobId: BlobID, patches: JsonPatch[]): Promise { + await this.client.patchJson(`${cardId}/messages/${blobId}`, patches, undefined, this.retryOptions) + } + + private deserializeMessageGroup (group: MessagesGroupDoc): MessagesGroup { + return { + cardId: group.cardId, + blobId: group.blobId, + fromDate: new Date(group.fromDate), + toDate: new Date(group.toDate), + count: group.count + } + } + + private serializeMessage (message: Message): MessageDoc { + return { + ...message, + language: message.language ?? null, + extra: message.extra ?? {}, + created: message.created.toISOString(), + modified: message.modified?.toISOString() ?? null, + reactions: {}, + attachments: {}, + threads: {} + } + } +} + +function matchDate (date: Date, filter: Partial> | Date): boolean { + const ts = date.getTime() + if (filter instanceof Date) return ts === filter.getTime() + + if (filter.greater != null && !(ts > filter.greater.getTime())) return false + if (filter.greaterOrEqual != null && !(ts >= filter.greaterOrEqual.getTime())) return false + if (filter.less != null && !(ts < filter.less.getTime())) return false + if (filter.lessOrEqual != null && !(ts <= filter.lessOrEqual.getTime())) return false + if (filter.notEqual != null && !(ts !== filter.notEqual.getTime())) return false + + return true +} diff --git a/foundations/communication/packages/server/src/client.ts b/foundations/communication/packages/server/src/client.ts new file mode 100644 index 0000000000..ce2ab8795b --- /dev/null +++ b/foundations/communication/packages/server/src/client.ts @@ -0,0 +1,112 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { DbAdapter } from '@hcengineering/communication-sdk-types' +import type { + CardID, FindMessagesOptions, + Message, + MessageID, + MessageMeta, + PersonUuid, + SocialID, + WorkspaceUuid +} from '@hcengineering/communication-types' +import { generateToken } from '@hcengineering/server-token' +import { Account, MeasureContext, systemAccountUuid } from '@hcengineering/core' +import { getClient as getAccountClient } from '@hcengineering/account-client' +import { loadMessages } from '@hcengineering/communication-shared' + +import { Blob } from './blob' +import type { Metadata } from './types' +import { getWorkspaceClient, HulylakeWorkspaceClient } from '@hcengineering/hulylake-client' + +export class LowLevelClient { + private readonly messageMetaCache = new Map() + private readonly personUuidBySocialIdCache = new Map() + private readonly lake: HulylakeWorkspaceClient + + constructor ( + readonly db: DbAdapter, + readonly blob: Blob, + private readonly metadata: Metadata, + private readonly workspace: WorkspaceUuid + ) { + this.lake = getWorkspaceClient(metadata.hulylakeUrl, workspace, generateToken(systemAccountUuid, workspace, undefined, metadata.secret)) + } + + async findMessage (cardId: CardID, messageId: MessageID, options?: FindMessagesOptions): Promise { + const meta = await this.getMessageMeta(cardId, messageId) + if (meta === undefined) { + return undefined + } + + return (await loadMessages(this.lake, meta.blobId, { + cardId, + id: messageId + }, options))[0] + } + + async findPersonUuid (ctx: { + ctx: MeasureContext + account: Account + }, socialId: SocialID, requireAccount: boolean = false): Promise { + if (ctx.account.socialIds.includes(socialId)) { + return ctx.account.uuid + } + const cached = this.personUuidBySocialIdCache.get(socialId) + if (cached !== undefined) { + return cached + } + + const url = this.metadata.accountsUrl ?? '' + if (url === '') return undefined + + const token = generateToken(systemAccountUuid, this.workspace, undefined, this.metadata.secret) + const accountClient = getAccountClient(this.metadata.accountsUrl, token) + + try { + const personUuid = await accountClient.findPersonBySocialId(socialId, requireAccount) + + if (personUuid !== undefined) { + this.personUuidBySocialIdCache.set(socialId, personUuid) + } + + return personUuid + } catch (err: any) { + ctx.ctx.warn('Cannot find person uuid', { socialString: socialId, err }) + } + } + + async getMessageMeta (cardId: CardID, messageId: MessageID): Promise { + const key = this.getMessageMetaKey(cardId, messageId) + if (this.messageMetaCache.has(key)) { + return this.messageMetaCache.get(key) + } + const meta = (await this.db.findMessagesMeta({ cardId, id: messageId }))[0] + if (meta === undefined) { + return undefined + } + this.messageMetaCache.set(key, meta) + return meta + } + + private getMessageMetaKey (cardId: CardID, messageId: MessageID): string { + return `${cardId}-${messageId}` + } + + async removeMessageMeta (cardId: CardID, messageId: MessageID): Promise { + await this.db.removeMessageMeta(cardId, messageId) + const key = this.getMessageMetaKey(cardId, messageId) + this.messageMetaCache.delete(key) + } +} diff --git a/foundations/communication/packages/server/src/error.ts b/foundations/communication/packages/server/src/error.ts new file mode 100644 index 0000000000..dea62e599b --- /dev/null +++ b/foundations/communication/packages/server/src/error.ts @@ -0,0 +1,49 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export class ApiError extends Error { + public code: number + public message: string + + private constructor (code: number, message: string) { + super(message) + this.code = code + this.message = message + Object.setPrototypeOf(this, ApiError.prototype) + } + + static badRequest (message: string): ApiError { + return new ApiError(400, `Bad Request: ${message}`) + } + + static forbidden (message: string): ApiError { + return new ApiError(403, `Forbidden: ${message}`) + } + + static notFound (message: string): ApiError { + return new ApiError(404, `Not Found: ${message}`) + } + + toJSON (): object { + return { + code: this.code, + message: this.message + } + } + + toString (): string { + return JSON.stringify(this.toJSON()) + } +} diff --git a/foundations/communication/packages/server/src/index.ts b/foundations/communication/packages/server/src/index.ts new file mode 100644 index 0000000000..6d0a45662d --- /dev/null +++ b/foundations/communication/packages/server/src/index.ts @@ -0,0 +1,118 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type MeasureContext } from '@hcengineering/core' +import type { + FindNotificationContextParams, + FindNotificationsParams, + NotificationContext, + WorkspaceUuid, + Notification, + FindLabelsParams, + Label, + FindCollaboratorsParams, + Collaborator, + FindPeersParams, + Peer, + CardID, FindMessagesMetaParams, MessageMeta, FindMessagesGroupParams, MessagesGroup +} from '@hcengineering/communication-types' +import { createDbAdapter } from '@hcengineering/communication-cockroach' +import type { EventResult, Event, ServerApi, SessionData } from '@hcengineering/communication-sdk-types' + +import { getMetadata } from './metadata' +import type { CommunicationCallbacks, Subscription } from './types' +import { buildMiddlewares, Middlewares } from './middlewares' +import { Blob } from './blob' +import { LowLevelClient } from './client' + +export class Api implements ServerApi { + private constructor ( + private readonly ctx: MeasureContext, + private readonly middlewares: Middlewares + ) {} + + static async create ( + ctx: MeasureContext, + workspace: WorkspaceUuid, + dbUrl: string, + callbacks: CommunicationCallbacks + ): Promise { + const metadata = getMetadata() + const db = await createDbAdapter(dbUrl, workspace, ctx, { + withLogs: process.env.COMMUNICATION_TIME_LOGGING_ENABLED === 'true' + }) + const blob = new Blob(ctx, workspace, metadata) + const client: LowLevelClient = new LowLevelClient(db, blob, metadata, workspace) + const middleware = await buildMiddlewares(ctx, workspace, metadata, client, callbacks) + + return new Api(ctx, middleware) + } + + async findMessagesMeta (session: SessionData, params: FindMessagesMetaParams): Promise { + return await this.middlewares.findMessagesMeta(session, params) + } + + async findMessagesGroups (session: SessionData, params: FindMessagesGroupParams): Promise { + return await this.middlewares.findMessagesGroups(session, params) + } + + async findNotificationContexts ( + session: SessionData, + params: FindNotificationContextParams, + subscription?: Subscription + ): Promise { + return await this.middlewares.findNotificationContexts(session, params, subscription) + } + + async findNotifications ( + session: SessionData, + params: FindNotificationsParams, + subscription?: Subscription + ): Promise { + return await this.middlewares.findNotifications(session, params, subscription) + } + + async findLabels (session: SessionData, params: FindLabelsParams): Promise { + return await this.middlewares.findLabels(session, params) + } + + async findCollaborators (session: SessionData, params: FindCollaboratorsParams): Promise { + return await this.middlewares.findCollaborators(session, params) + } + + async findPeers (session: SessionData, params: FindPeersParams): Promise { + return await this.middlewares.findPeers(session, params) + } + + subscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { + this.middlewares.subscribeCard(session, cardId, subscription) + } + + unsubscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { + this.middlewares.unsubscribeCard(session, cardId, subscription) + } + + async event (session: SessionData, event: Event): Promise { + return await this.middlewares.event(session, event) + } + + async closeSession (sessionId: string): Promise { + await this.middlewares.closeSession(sessionId) + } + + async close (): Promise { + await this.middlewares.close() + } +} diff --git a/foundations/communication/packages/server/src/messageId.ts b/foundations/communication/packages/server/src/messageId.ts new file mode 100644 index 0000000000..8cc6fb7a6f --- /dev/null +++ b/foundations/communication/packages/server/src/messageId.ts @@ -0,0 +1,34 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MessageID } from '@hcengineering/communication-types' + +const EPOCH_OFFSET_US = BigInt(Date.UTC(2022, 0, 1)) * 1000n +const monoStartNs = process.hrtime.bigint() +const realStartUs = BigInt(Date.now()) * 1000n +let lastTick = 0n + +function getMonotonicTick10us (): bigint { + const nowNs = process.hrtime.bigint() + const deltaUs = (nowNs - monoStartNs) / 1000n + const absUs = realStartUs + deltaUs + const relUs = absUs > EPOCH_OFFSET_US ? absUs - EPOCH_OFFSET_US : 0n + const candidate = relUs / 10n + const tick = candidate <= lastTick ? lastTick + 1n : candidate + lastTick = tick + return tick & ((1n << 64n) - 1n) +} + +export function generateMessageId (): MessageID { + return getMonotonicTick10us().toString() as MessageID +} diff --git a/foundations/communication/packages/server/src/metadata.ts b/foundations/communication/packages/server/src/metadata.ts new file mode 100644 index 0000000000..09f8c73ae5 --- /dev/null +++ b/foundations/communication/packages/server/src/metadata.ts @@ -0,0 +1,25 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Metadata } from './types' + +export function getMetadata (): Metadata { + return { + accountsUrl: process.env.ACCOUNTS_URL ?? '', + secret: process.env.SERVER_SECRET ?? 'secret', + hulylakeUrl: process.env.HULYLAKE_URL ?? 'http://huly.local:8096', + messagesPerBlob: Number(process.env.MESSAGES_PER_BLOB ?? '200') + } +} diff --git a/foundations/communication/packages/server/src/middleware/base.ts b/foundations/communication/packages/server/src/middleware/base.ts new file mode 100644 index 0000000000..806a9b8ec5 --- /dev/null +++ b/foundations/communication/packages/server/src/middleware/base.ts @@ -0,0 +1,184 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Event, EventResult, type SessionData } from '@hcengineering/communication-sdk-types' +import type { + FindNotificationContextParams, + FindNotificationsParams, + NotificationContext, + Notification, + FindLabelsParams, + Label, + FindCollaboratorsParams, + Collaborator, + FindPeersParams, + Peer, + CardID, + FindMessagesMetaParams, + MessageMeta, + FindMessagesGroupParams, + MessagesGroup +} from '@hcengineering/communication-types' + +import type { Enriched, Middleware, MiddlewareContext, Subscription } from '../types' + +export class BaseMiddleware implements Middleware { + constructor ( + readonly context: MiddlewareContext, + protected readonly next?: Middleware + ) {} + + async findMessagesMeta (session: SessionData, params: FindMessagesMetaParams): Promise { + return await this.provideFindMessagesMeta(session, params) + } + + async findMessagesGroups (session: SessionData, params: FindMessagesGroupParams): Promise { + return await this.provideFindMessagesGroups(session, params) + } + + async findNotificationContexts ( + session: SessionData, + params: FindNotificationContextParams, + subscription?: Subscription + ): Promise { + return await this.provideFindNotificationContexts(session, params, subscription) + } + + async findNotifications ( + session: SessionData, + params: FindNotificationsParams, + subscription?: Subscription + ): Promise { + return await this.provideFindNotifications(session, params, subscription) + } + + async findLabels (session: SessionData, params: FindLabelsParams, subscription?: Subscription): Promise { + return await this.provideFindLabels(session, params, subscription) + } + + async findCollaborators (session: SessionData, params: FindCollaboratorsParams): Promise { + return await this.provideFindCollaborators(session, params) + } + + async findPeers (session: SessionData, params: FindPeersParams): Promise { + return await this.provideFindPeers(session, params) + } + + async event (session: SessionData, event: Enriched, derived: boolean): Promise { + return await this.provideEvent(session, event, derived) + } + + handleBroadcast (session: SessionData, events: Enriched[]): void { + this.provideHandleBroadcast(session, events) + } + + subscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { + if (this.next !== undefined) { + this.next.subscribeCard(session, cardId, subscription) + } + } + + unsubscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { + if (this.next !== undefined) { + this.next.unsubscribeCard(session, cardId, subscription) + } + } + + close (): void {} + closeSession (sessionId: string): void {} + + protected async provideEvent (session: SessionData, event: Enriched, derived: boolean): Promise { + if (this.next !== undefined) { + return await this.next.event(session, event, derived) + } + return {} + } + + protected async provideFindMessagesGroups ( + session: SessionData, + params: FindMessagesGroupParams + ): Promise { + if (this.next !== undefined) { + return await this.next.findMessagesGroups(session, params) + } + return [] + } + + protected async provideFindMessagesMeta ( + session: SessionData, + params: FindMessagesMetaParams + ): Promise { + if (this.next !== undefined) { + return await this.next.findMessagesMeta(session, params) + } + return [] + } + + protected async provideFindNotificationContexts ( + session: SessionData, + params: FindNotificationContextParams, + subscription?: Subscription + ): Promise { + if (this.next !== undefined) { + return await this.next.findNotificationContexts(session, params, subscription) + } + return [] + } + + protected async provideFindNotifications ( + session: SessionData, + params: FindNotificationsParams, + subscription?: Subscription + ): Promise { + if (this.next !== undefined) { + return await this.next.findNotifications(session, params, subscription) + } + return [] + } + + protected async provideFindLabels ( + session: SessionData, + params: FindLabelsParams, + subscription?: Subscription + ): Promise { + if (this.next !== undefined) { + return await this.next.findLabels(session, params, subscription) + } + return [] + } + + protected async provideFindCollaborators ( + session: SessionData, + params: FindCollaboratorsParams + ): Promise { + if (this.next !== undefined) { + return await this.next.findCollaborators(session, params) + } + return [] + } + + protected async provideFindPeers (session: SessionData, params: FindPeersParams): Promise { + if (this.next !== undefined) { + return await this.next.findPeers(session, params) + } + return [] + } + + protected provideHandleBroadcast (session: SessionData, events: Enriched[]): void { + if (this.next !== undefined) { + this.next.handleBroadcast(session, events) + } + } +} diff --git a/foundations/communication/packages/server/src/middleware/broadcast.ts b/foundations/communication/packages/server/src/middleware/broadcast.ts new file mode 100644 index 0000000000..c68c698604 --- /dev/null +++ b/foundations/communication/packages/server/src/middleware/broadcast.ts @@ -0,0 +1,199 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + CardEventType, + type Event, + EventResult, + LabelEventType, + MessageEventType, + NotificationEventType, + PeerEventType, + type SessionData +} from '@hcengineering/communication-sdk-types' +import type { + AccountUuid, + CardID, + FindLabelsParams, + FindNotificationContextParams, + FindNotificationsParams, + Label, + Notification, + NotificationContext +} from '@hcengineering/communication-types' + +import type { CommunicationCallbacks, Enriched, Middleware, MiddlewareContext, Subscription } from '../types' +import { BaseMiddleware } from './base' + +interface SessionInfo { + account: AccountUuid + subscriptions: Map> +} + +export class BroadcastMiddleware extends BaseMiddleware implements Middleware { + private readonly dataBySessionId = new Map() + + constructor ( + private readonly callbacks: CommunicationCallbacks, + readonly context: MiddlewareContext, + next?: Middleware + ) { + super(context, next) + } + + async findNotificationContexts ( + session: SessionData, + params: FindNotificationContextParams, + subscription?: Subscription + ): Promise { + this.createSession(session) + + const result = await this.provideFindNotificationContexts(session, params, subscription) + if (subscription != null && session.sessionId != null && session.sessionId !== '') { + this.subscribeContextsCard(session, subscription, result) + } + return result + } + + async findNotifications ( + session: SessionData, + params: FindNotificationsParams, + queryId?: Subscription + ): Promise { + this.createSession(session) + return await this.provideFindNotifications(session, params, queryId) + } + + async findLabels (session: SessionData, params: FindLabelsParams, queryId?: Subscription): Promise { + this.createSession(session) + return await this.provideFindLabels(session, params, queryId) + } + + async event (session: SessionData, event: Enriched, derived: boolean): Promise { + this.createSession(session) + return await this.provideEvent(session, event, derived) + } + + unsubscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { + if (session.sessionId == null) return + const data = this.dataBySessionId.get(session.sessionId) + if (data == null) return + + const current = data.subscriptions.get(cardId) + if (current == null) return + current.delete(subscription) + + data.subscriptions.set(cardId, current) + } + + subscribeCard (session: SessionData, cardId: CardID, subscription: string | number): void { + if (session.sessionId == null) return + const data = this.dataBySessionId.get(session.sessionId) + if (data == null) return + + const current = data.subscriptions.get(cardId) ?? new Set() + current.add(subscription) + data.subscriptions.set(cardId, current) + } + + handleBroadcast (session: SessionData, events: Enriched[]): void { + if (events.length === 0) return + const sessionIds: Record[]> = {} + + for (const [sessionId, session] of this.dataBySessionId.entries()) { + sessionIds[sessionId] = events.filter((it) => this.match(it, session)) + } + + const ctx = this.context.ctx.newChild('enqueue', {}) + ctx.contextData = session.contextData + + if (Object.keys(sessionIds).length > 0) { + try { + this.callbacks.broadcast(ctx, sessionIds) + } catch (e) { + this.context.ctx.error('Failed to broadcast event', { error: e }) + } + } + + try { + this.callbacks.enqueue(ctx, events) + } catch (e) { + this.context.ctx.error('Failed to broadcast event', { error: e }) + } + } + + closeSession (sessionId: string): void { + this.dataBySessionId.delete(sessionId) + } + + close (): void { + this.dataBySessionId.clear() + } + + private subscribeContextsCard (session: SessionData, queryId: Subscription, result: NotificationContext[]): void { + const data = this.createSession(session) + if (data == null) return + + for (const context of result) { + this.subscribeCard(session, context.cardId, queryId) + } + } + + private createSession (session: SessionData): SessionInfo | undefined { + const id = session.sessionId + if (id == null) return + if (!this.dataBySessionId.has(id)) { + this.dataBySessionId.set(id, { + account: session.account.uuid, + subscriptions: new Map() + }) + } + + return this.dataBySessionId.get(id) + } + + private match (event: Enriched, info: SessionInfo): boolean { + switch (event.type) { + case MessageEventType.CreateMessage: + case MessageEventType.ThreadPatch: + case MessageEventType.ReactionPatch: + case MessageEventType.BlobPatch: + case MessageEventType.AttachmentPatch: + case MessageEventType.RemovePatch: + case MessageEventType.UpdatePatch: + case MessageEventType.TranslateMessage: + return info.subscriptions.has(event.cardId) + case NotificationEventType.RemoveNotifications: + case NotificationEventType.CreateNotification: + case NotificationEventType.UpdateNotification: + case NotificationEventType.RemoveNotificationContext: + case NotificationEventType.UpdateNotificationContext: + case NotificationEventType.CreateNotificationContext: + return info.account === event.account + case NotificationEventType.RemoveCollaborators: + case NotificationEventType.AddCollaborators: + return true + case LabelEventType.CreateLabel: + case LabelEventType.RemoveLabel: + return info.account === event.account + case CardEventType.UpdateCardType: + case CardEventType.RemoveCard: + return true + case PeerEventType.RemovePeer: + case PeerEventType.CreatePeer: + return false + } + } +} diff --git a/foundations/communication/packages/server/src/middleware/date.ts b/foundations/communication/packages/server/src/middleware/date.ts new file mode 100644 index 0000000000..ecf7f85c6d --- /dev/null +++ b/foundations/communication/packages/server/src/middleware/date.ts @@ -0,0 +1,46 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { EventResult, type Event, type SessionData } from '@hcengineering/communication-sdk-types' +import { systemAccountUuid } from '@hcengineering/core' + +import type { Middleware, MiddlewareContext, Enriched } from '../types' +import { BaseMiddleware } from './base' + +export class DateMiddleware extends BaseMiddleware implements Middleware { + constructor ( + readonly context: MiddlewareContext, + next?: Middleware + ) { + super(context, next) + } + + async event (session: SessionData, event: Enriched, derived: boolean): Promise { + const canSetDate = derived || this.isSystem(session) + + if (!canSetDate || event.date == null) { + event.date = new Date() + } + + event._eventExtra = event._eventExtra ?? {} + + return await this.provideEvent(session, event, derived) + } + + private isSystem (session: SessionData): boolean { + const account = session.account + return systemAccountUuid === account.uuid + } +} diff --git a/foundations/communication/packages/server/src/middleware/id.ts b/foundations/communication/packages/server/src/middleware/id.ts new file mode 100644 index 0000000000..137295f24a --- /dev/null +++ b/foundations/communication/packages/server/src/middleware/id.ts @@ -0,0 +1,39 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { EventResult, MessageEventType, type Event, type SessionData } from '@hcengineering/communication-sdk-types' + +import { generateMessageId } from '../messageId' +import type { Middleware, MiddlewareContext, Enriched } from '../types' +import { BaseMiddleware } from './base' + +export class IdMiddleware extends BaseMiddleware implements Middleware { + constructor ( + readonly context: MiddlewareContext, + next?: Middleware + ) { + super(context, next) + } + + async event (session: SessionData, event: Enriched, derived: boolean): Promise { + if (event.type === MessageEventType.CreateMessage) { + if (event.messageId == null) { + event.messageId = generateMessageId() + } + } + + return await this.provideEvent(session, event, derived) + } +} diff --git a/foundations/communication/packages/server/src/middleware/indentity.ts b/foundations/communication/packages/server/src/middleware/indentity.ts new file mode 100644 index 0000000000..c8a0088fb6 --- /dev/null +++ b/foundations/communication/packages/server/src/middleware/indentity.ts @@ -0,0 +1,99 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Event, EventResult, MessageEventType, type SessionData } from '@hcengineering/communication-sdk-types' +import { systemAccountUuid } from '@hcengineering/core' +import type { + AccountUuid, + FindLabelsParams, + FindNotificationContextParams, + FindNotificationsParams, + Label, + Notification, + NotificationContext +} from '@hcengineering/communication-types' + +import type { Enriched, Middleware, MiddlewareContext, Subscription } from '../types' +import { BaseMiddleware } from './base' + +export class IdentityMiddleware extends BaseMiddleware implements Middleware { + constructor ( + readonly context: MiddlewareContext, + next?: Middleware + ) { + super(context, next) + } + + async event (session: SessionData, event: Enriched, derived: boolean): Promise { + switch (event.type) { + case MessageEventType.ThreadPatch: + case MessageEventType.ReactionPatch: { + const personUuid = await this.context.client.findPersonUuid( + { + ctx: this.context.ctx, + account: session.account + }, + event.socialId + ) + event.personUuid = personUuid + break + } + default: + break + } + + return await this.provideEvent(session, event, derived) + } + + async findNotificationContexts ( + session: SessionData, + params: FindNotificationContextParams, + subscription?: Subscription + ): Promise { + const paramsWithAccount = this.enrichParamsWithAccount(session, params) + return await this.provideFindNotificationContexts(session, paramsWithAccount, subscription) + } + + async findNotifications ( + session: SessionData, + params: FindNotificationsParams, + subscription?: Subscription + ): Promise { + const paramsWithAccount = this.enrichParamsWithAccount(session, params) + return await this.provideFindNotifications(session, paramsWithAccount, subscription) + } + + async findLabels (session: SessionData, params: FindLabelsParams, subscription?: Subscription): Promise { + const paramsWithAccount = this.enrichParamsWithAccount(session, params) + return await this.provideFindLabels(session, paramsWithAccount, subscription) + } + + private enrichParamsWithAccount( + session: SessionData, + params: T + ): T { + const account = session.account + const isSystem = account.uuid === systemAccountUuid + + if (isSystem) { + return params + } + + return { + ...params, + account: account.uuid + } + } +} diff --git a/foundations/communication/packages/server/src/middleware/peer.ts b/foundations/communication/packages/server/src/middleware/peer.ts new file mode 100644 index 0000000000..b9efe1777a --- /dev/null +++ b/foundations/communication/packages/server/src/middleware/peer.ts @@ -0,0 +1,60 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type Event, + EventResult, + MessageEventType, + PeerEventType, + type SessionData +} from '@hcengineering/communication-sdk-types' + +import type { Enriched, Middleware, MiddlewareContext } from '../types' +import { BaseMiddleware } from './base' + +export class PeerMiddleware extends BaseMiddleware implements Middleware { + constructor ( + readonly context: MiddlewareContext, + next?: Middleware + ) { + super(context, next) + } + + async event (session: SessionData, event: Enriched, derived: boolean): Promise { + switch (event.type) { + case PeerEventType.CreatePeer: + this.context.cadsWithPeers.add(event.cardId) + break + case MessageEventType.CreateMessage: + case MessageEventType.UpdatePatch: + case MessageEventType.RemovePatch: + case MessageEventType.AttachmentPatch: + case MessageEventType.ReactionPatch: + case MessageEventType.ThreadPatch: + case MessageEventType.BlobPatch: { + if (this.context.cadsWithPeers.has(event.cardId)) { + event._eventExtra.peers = + (await this.context.head?.findPeers(session, { + workspaceId: this.context.workspace, + cardId: event.cardId + })) ?? [] + } + break + } + } + + return await this.provideEvent(session, event, derived) + } +} diff --git a/foundations/communication/packages/server/src/middleware/permissions.ts b/foundations/communication/packages/server/src/middleware/permissions.ts new file mode 100644 index 0000000000..0ccaa2dcba --- /dev/null +++ b/foundations/communication/packages/server/src/middleware/permissions.ts @@ -0,0 +1,134 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type Event, + EventResult, + MessageEventType, + NotificationEventType, + PeerEventType, + type SessionData +} from '@hcengineering/communication-sdk-types' +import { AccountRole, systemAccountUuid } from '@hcengineering/core' +import type { AccountUuid, CardID, MessageID, SocialID } from '@hcengineering/communication-types' + +import { ApiError } from '../error' +import type { Enriched, Middleware, MiddlewareContext } from '../types' +import { BaseMiddleware } from './base' + +export class PermissionsMiddleware extends BaseMiddleware implements Middleware { + constructor ( + readonly context: MiddlewareContext, + next?: Middleware + ) { + super(context, next) + } + + async event (session: SessionData, event: Enriched, derived: boolean): Promise { + if (derived) return await this.provideEvent(session, event, derived) + + this.notAnonymousAccount(session) + + if (this.isSystemAccount(session)) { + return await this.provideEvent(session, event, derived) + } + + switch (event.type) { + case MessageEventType.CreateMessage: + this.checkSocialId(session, event.socialId) + if (!this.isSystemAccount(session) && event?.options?.noNotify === true) { + event.options.noNotify = false + } + break + case MessageEventType.RemovePatch: + case MessageEventType.UpdatePatch: + case MessageEventType.BlobPatch: + case MessageEventType.AttachmentPatch: + this.checkSocialId(session, event.socialId) + await this.checkMessageAuthor(session, event.cardId, event.messageId) + break + case MessageEventType.ReactionPatch: + case MessageEventType.ThreadPatch: + case NotificationEventType.AddCollaborators: + case NotificationEventType.RemoveCollaborators: + this.checkSocialId(session, event.socialId) + break + case NotificationEventType.RemoveNotifications: + case NotificationEventType.UpdateNotificationContext: + case NotificationEventType.UpdateNotification: + case NotificationEventType.RemoveNotificationContext: { + this.checkAccount(session, event.account) + break + } + case MessageEventType.TranslateMessage: + case PeerEventType.CreatePeer: + case PeerEventType.RemovePeer: { + this.onlySystemAccount(session) + break + } + default: + break + } + + return await this.provideEvent(session, event, derived) + } + + private async checkMessageAuthor (session: SessionData, cardId: CardID, messageId: MessageID): Promise { + const meta = await this.context.client.getMessageMeta(cardId, messageId) + if (meta === undefined) { + throw ApiError.notFound(`message not found: cardId =${cardId}, messageId = ${messageId}`) + } + + if (!session.account.socialIds.includes(meta.creator)) { + throw ApiError.forbidden('message author is not allowed') + } + } + + private checkSocialId (session: SessionData, creator: SocialID): void { + const account = session.account + if (!account.socialIds.includes(creator) && systemAccountUuid !== account.uuid) { + throw ApiError.forbidden('social ID is not allowed') + } + } + + private checkAccount (session: SessionData, creator: AccountUuid): void { + const account = session.account + if (account.uuid !== creator && systemAccountUuid !== account.uuid) { + throw ApiError.forbidden('account is not allowed') + } + } + + private onlySystemAccount (session: SessionData): void { + if (!this.isSystemAccount(session)) { + throw ApiError.forbidden('only system account is allowed') + } + } + + private notAnonymousAccount (session: SessionData): void { + if (this.isAnonymousAccount(session)) { + throw ApiError.forbidden('anonymous account is not allowed') + } + } + + private isSystemAccount (session: SessionData): boolean { + const account = session.account + return systemAccountUuid === account.uuid + } + + private isAnonymousAccount (session: SessionData): boolean { + const account = session.account + return account.role === AccountRole.ReadOnlyGuest + } +} diff --git a/foundations/communication/packages/server/src/middleware/storage.ts b/foundations/communication/packages/server/src/middleware/storage.ts new file mode 100644 index 0000000000..6a2c103db9 --- /dev/null +++ b/foundations/communication/packages/server/src/middleware/storage.ts @@ -0,0 +1,614 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + Attachment, + AttachmentID, + type Collaborator, + type FindCollaboratorsParams, + type FindLabelsParams, + type FindNotificationContextParams, + type FindNotificationsParams, + FindPeersParams, + FindThreadMetaParams, + type Label, + type Notification, + type NotificationContext, + Peer, + Thread, + ThreadMeta, + FindMessagesMetaParams, + MessageMeta, + FindMessagesGroupParams, + MessagesGroup +} from '@hcengineering/communication-types' +import { + type AddCollaboratorsEvent, + AttachmentPatchEvent, + BlobPatchEvent, + CardEventType, + type CreateLabelEvent, + type CreateMessageEvent, + CreateMessageResult, + type CreateNotificationContextEvent, + type CreateNotificationEvent, + CreatePeerEvent, + type DbAdapter, + type Event, + EventResult, + LabelEventType, + MessageEventType, + NotificationEventType, + PeerEventType, + ReactionPatchEvent, + type RemoveCollaboratorsEvent, + type RemoveLabelEvent, + type RemoveNotificationContextEvent, + type RemoveNotificationsEvent, + RemovePatchEvent, + RemovePeerEvent, + type SessionData, + ThreadPatchEvent, + type UpdateNotificationContextEvent, + type UpdateNotificationEvent, + UpdatePatchEvent +} from '@hcengineering/communication-sdk-types' +import { MessageProcessor } from '@hcengineering/communication-shared' +import { + AddAttachmentsOperation, + RemoveAttachmentsOperation, + SetAttachmentsOperation, + UpdateAttachmentsOperation +} from '@hcengineering/communication-sdk-types' + +import type { Enriched, Middleware, MiddlewareContext } from '../types' +import { BaseMiddleware } from './base' +import { Blob } from '../blob' + +interface Result { + skipPropagate?: boolean + result?: EventResult +} + +export class StorageMiddleware extends BaseMiddleware implements Middleware { + private readonly blob: Blob + private readonly db: DbAdapter + constructor ( + readonly context: MiddlewareContext, + next?: Middleware + ) { + super(context, next) + + this.blob = context.client.blob + this.db = context.client.db + } + + async findMessagesMeta (session: SessionData, params: FindMessagesMetaParams): Promise { + return await this.db.findMessagesMeta(params) + } + + async findMessagesGroups (session: SessionData, params: FindMessagesGroupParams): Promise { + if (params.id != null) { + const meta = await this.context.client.getMessageMeta(params.cardId, params.id) + if (meta == null) return [] + return await this.blob.findMessagesGroups({ + ...params, + blobId: params.blobId ?? meta.blobId + }) + } + return await this.blob.findMessagesGroups(params) + } + + async findNotificationContexts ( + _: SessionData, + params: FindNotificationContextParams + ): Promise { + return await this.db.findNotificationContexts(params) + } + + async findNotifications (_: SessionData, params: FindNotificationsParams): Promise { + return await this.db.findNotifications(params) + } + + async findLabels (_: SessionData, params: FindLabelsParams): Promise { + return await this.db.findLabels(params) + } + + async findCollaborators (_: SessionData, params: FindCollaboratorsParams): Promise { + return await this.db.findCollaborators(params) + } + + async findPeers (_: SessionData, params: FindPeersParams): Promise { + return await this.db.findPeers(params) + } + + async findThreadMeta (_: SessionData, params: FindThreadMetaParams): Promise { + return await this.db.findThreadMeta(params) + } + + async event (session: SessionData, event: Enriched, derived: boolean): Promise { + const result = await this.processEvent(session, event) + + if (result.skipPropagate === true) { + event.skipPropagate = true + } else { + await this.provideEvent(session, event, derived) + } + + return result.result ?? {} + } + + private async processEvent (session: SessionData, event: Enriched): Promise { + switch (event.type) { + // Messages + case MessageEventType.CreateMessage: + return await this.createMessage(event) + case MessageEventType.UpdatePatch: + return await this.updatePatch(event) + case MessageEventType.RemovePatch: + return await this.removePatch(event) + case MessageEventType.TranslateMessage: + return {} + + case MessageEventType.ReactionPatch: + return await this.reactionPatch(event, session) + case MessageEventType.BlobPatch: + return await this.blobPatch(event) + case MessageEventType.AttachmentPatch: + return await this.attachmentPatch(event) + case MessageEventType.ThreadPatch: + return await this.threadPatch(event, session) + + // Labels + case LabelEventType.CreateLabel: + return await this.createLabel(event) + case LabelEventType.RemoveLabel: + return await this.removeLabel(event) + + // Cards + case CardEventType.UpdateCardType: + case CardEventType.RemoveCard: + return {} + + // Peers + case PeerEventType.RemovePeer: + return await this.removePeer(event) + case PeerEventType.CreatePeer: + return await this.createPeer(event) + + // Collaborators + case NotificationEventType.AddCollaborators: + return await this.addCollaborators(event) + case NotificationEventType.RemoveCollaborators: + return await this.removeCollaborators(event) + + // Notifications + case NotificationEventType.CreateNotification: + return await this.createNotification(event) + case NotificationEventType.RemoveNotifications: + return await this.removeNotifications(event) + case NotificationEventType.UpdateNotification: + return await this.updateNotification(event) + + // Notification Contexts + case NotificationEventType.CreateNotificationContext: + return await this.createNotificationContext(event) + case NotificationEventType.RemoveNotificationContext: + return await this.removeNotificationContext(event) + case NotificationEventType.UpdateNotificationContext: + return await this.updateNotificationContext(event) + } + } + + private async addCollaborators (event: Enriched): Promise { + const added = await this.db.addCollaborators(event.cardId, event.cardType, event.collaborators, event.date) + + if (added.length === 0) return { skipPropagate: true } + event.collaborators = added + return {} + } + + private async removeCollaborators (event: Enriched): Promise { + if (event.collaborators.length === 0) return { skipPropagate: true } + await this.db.removeCollaborators({ cardId: event.cardId, account: event.collaborators }) + + return {} + } + + private async createMessage (event: Enriched): Promise { + if (event.messageId == null) { + throw new Error('Message id is required') + } + + const group = await this.blob.getMessageGroupByDate(event.cardId, event.date) + if (group == null) { + throw new Error( + `Cannot create message, group not found: cardId = ${event.cardId}, messageId = ${event.messageId}, created = ${event.date.toISOString()}` + ) + } + const result: CreateMessageResult = { + messageId: event.messageId, + created: event.date, + blobId: group.blobId + } + const created = await this.db.createMessageMeta( + event.cardId, + event.messageId, + event.socialId, + event.date, + group.blobId + ) + + if (!created) { + return { + skipPropagate: true, + result + } + } + await this.blob.insertMessage(event.cardId, group, MessageProcessor.create(event)) + + event._eventExtra.blobId = group.blobId + + return { + result + } + } + + private async updatePatch (event: Enriched): Promise { + const data = { + content: event.content, + extra: event.extra, + language: event.language + } + const meta = await this.context.client.getMessageMeta(event.cardId, event.messageId) + + if (meta === undefined) { + return { skipPropagate: true } + } + + await this.blob.updateMessage(event.cardId, meta.blobId, event.messageId, data, event.date) + event._eventExtra.blobId = meta.blobId + + return {} + } + + private async removePatch (event: Enriched): Promise { + const meta = await this.context.client.getMessageMeta(event.cardId, event.messageId) + + if (meta === undefined) { + return { skipPropagate: true } + } + await this.blob.removeMessage(event.cardId, meta.blobId, event.messageId) + await this.context.client.removeMessageMeta(event.cardId, event.messageId) + event._eventExtra.blobId = meta.blobId + return {} + } + + private async reactionPatch (event: Enriched, session: SessionData): Promise { + const meta = await this.context.client.getMessageMeta(event.cardId, event.messageId) + + if (meta === undefined) { + return { skipPropagate: true } + } + + const { operation, personUuid } = event + + if (personUuid === undefined) { + return { skipPropagate: true } + } + + if (operation.opcode === 'add') { + await this.blob.addReaction( + event.cardId, + meta.blobId, + event.messageId, + operation.reaction, + personUuid, + event.date + ) + } else if (operation.opcode === 'remove') { + await this.blob.removeReaction(event.cardId, meta.blobId, event.messageId, operation.reaction, personUuid) + } + + return {} + } + + private async blobPatch (event: Enriched): Promise { + const { operations } = event + + const attachmentOperations: ( + | AddAttachmentsOperation + | RemoveAttachmentsOperation + | SetAttachmentsOperation + | UpdateAttachmentsOperation + )[] = [] + + for (const operation of operations) { + if (operation.opcode === 'attach') { + attachmentOperations.push({ + opcode: 'add', + attachments: operation.blobs.map((b) => ({ + id: b.blobId as any as AttachmentID, + mimeType: b.mimeType, + params: b + })) + }) + } else if (operation.opcode === 'detach') { + attachmentOperations.push({ + opcode: 'remove', + ids: operation.blobIds as any as AttachmentID[] + }) + } else if (operation.opcode === 'set') { + attachmentOperations.push({ + opcode: 'set', + attachments: operation.blobs.map((b) => ({ + id: b.blobId as any as AttachmentID, + mimeType: b.mimeType, + params: b + })) + }) + } else if (operation.opcode === 'update') { + attachmentOperations.push({ + opcode: 'update', + attachments: operation.blobs.map((b) => ({ + id: b.blobId as any as AttachmentID, + params: { ...b } + })) + }) + } + } + + if (attachmentOperations.length === 0) { + return { skipPropagate: true } + } + + await this.attachmentPatch({ + _id: event._id, + type: MessageEventType.AttachmentPatch, + cardId: event.cardId, + messageId: event.messageId, + operations: attachmentOperations, + socialId: event.socialId, + date: event.date, + _eventExtra: event._eventExtra + }) + + return {} + } + + private async attachmentPatch (event: Enriched): Promise { + const meta = await this.context.client.getMessageMeta(event.cardId, event.messageId) + if (meta === undefined) { + return { skipPropagate: true } + } + + const { operations } = event + + for (const operation of operations) { + if (operation.opcode === 'add') { + const attachments: Attachment[] = operation.attachments.map( + (it) => + ({ + ...it, + created: event.date, + creator: event.socialId + }) as any + ) + await this.blob.addAttachments(event.cardId, meta.blobId, event.messageId, attachments) + } else if (operation.opcode === 'remove') { + await this.blob.removeAttachments(event.cardId, meta.blobId, event.messageId, operation.ids) + } else if (operation.opcode === 'set') { + const attachments: Attachment[] = operation.attachments.map( + (it) => + ({ + ...it, + created: event.date, + creator: event.socialId + }) as any + ) + await this.blob.setAttachments(event.cardId, meta.blobId, event.messageId, attachments) + } else if (operation.opcode === 'update') { + await this.blob.updateAttachments(event.cardId, meta.blobId, event.messageId, operation.attachments, event.date) + } + } + + return {} + } + + private async threadPatch (event: Enriched, session: SessionData): Promise { + const meta = await this.context.client.getMessageMeta(event.cardId, event.messageId) + if (meta === undefined) { + return { skipPropagate: true } + } + + if (event.operation.opcode === 'attach') { + const thread: Thread = { + cardId: event.operation.threadId, + messageId: event.messageId, + threadId: event.operation.threadId, + threadType: event.operation.threadType, + repliesCount: 0, + lastReplyDate: new Date(), + repliedPersons: {} + } + await this.db.attachThreadMeta( + event.cardId, + event.messageId, + thread.threadId, + thread.threadType, + event.socialId, + event.date + ) + await this.blob.attachThread(event.cardId, meta.blobId, event.messageId, thread) + } else if (event.operation.opcode === 'update') { + await this.blob.updateThread( + event.cardId, + meta.blobId, + event.messageId, + event.operation.threadId, + event.operation.update + ) + } else if (event.operation.opcode === 'addReply') { + const personUuid = await this.context.client.findPersonUuid( + { + ctx: this.context.ctx, + account: session.account + }, + event.socialId + ) + if (personUuid === undefined) return { skipPropagate: true } + await this.blob.addThreadReply( + event.cardId, + meta.blobId, + event.messageId, + event.operation.threadId, + personUuid, + event.date + ) + } else if (event.operation.opcode === 'removeReply') { + const personUuid = await this.context.client.findPersonUuid( + { + ctx: this.context.ctx, + account: session.account + }, + event.socialId + ) + + if (personUuid === undefined) return { skipPropagate: true } + await this.blob.removeThreadReply( + event.cardId, + meta.blobId, + event.messageId, + event.operation.threadId, + personUuid + ) + } + + return {} + } + + private async createNotification (event: Enriched): Promise { + const id = await this.db.createNotification( + event.contextId, + event.messageId, + event.blobId, + event.notificationType, + event.read ?? false, + event.content, + event.creator, + event.date + ) + + event.notificationId = id + + return {} + } + + private async updateNotification (event: Enriched): Promise { + const updated = await this.db.updateNotification( + { + contextId: event.contextId, + account: event.account, + ...event.query + }, + event.updates + ) + if (updated === 0) return { skipPropagate: true } + event.updated = updated + return {} + } + + private async removeNotifications (event: Enriched): Promise { + if (event.ids.length === 0) return { skipPropagate: true } + const ids = await this.db.removeNotifications({ + contextId: event.contextId, + account: event.account, + id: event.ids + }) + event.ids = ids + return { + result: { + ids + } + } + } + + private async createNotificationContext (event: Enriched): Promise { + const id = await this.db.createNotificationContext( + event.account, + event.cardId, + event.lastUpdate, + event.lastView, + event.lastNotify + ) + + event.contextId = id + return { + result: { id } + } + } + + private async removeNotificationContext (event: Enriched): Promise { + const id = await this.db.removeContext({ + id: event.contextId, + account: event.account + }) + + if (id == null) return { skipPropagate: true } + return {} + } + + async updateNotificationContext (event: Enriched): Promise { + await this.db.updateContext( + { + id: event.contextId, + account: event.account + }, + event.updates + ) + + return {} + } + + private async createLabel (event: Enriched): Promise { + await this.db.createLabel(event.cardId, event.cardType, event.labelId, event.account, event.date) + + return {} + } + + private async removeLabel (event: Enriched): Promise { + await this.db.removeLabels({ + labelId: event.labelId, + cardId: event.cardId, + account: event.account + }) + + return {} + } + + private async createPeer (event: Enriched): Promise { + await this.db.createPeer(event.workspaceId, event.cardId, event.kind, event.value, event.extra ?? {}, event.date) + return {} + } + + private async removePeer (event: Enriched): Promise { + await this.db.removePeer(event.workspaceId, event.cardId, event.kind, event.value) + return {} + } + + close (): void { + this.db.close() + } +} diff --git a/foundations/communication/packages/server/src/middleware/triggers.ts b/foundations/communication/packages/server/src/middleware/triggers.ts new file mode 100644 index 0000000000..d203be1c7e --- /dev/null +++ b/foundations/communication/packages/server/src/middleware/triggers.ts @@ -0,0 +1,159 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Event, EventResult, SessionData } from '@hcengineering/communication-sdk-types' +import type { MeasureContext } from '@hcengineering/core' + +import type { CommunicationCallbacks, Enriched, Middleware, MiddlewareContext, TriggerCtx } from '../types' +import { BaseMiddleware } from './base' +import triggers from '../triggers/all' +import { notify } from '../notification/notification' + +export class TriggersMiddleware extends BaseMiddleware implements Middleware { + private ctx: MeasureContext + private processedPeersEvents = new Set() + + constructor ( + private readonly callbacks: CommunicationCallbacks, + context: MiddlewareContext, + next?: Middleware + ) { + super(context, next) + this.ctx = context.ctx.newChild('triggers', {}) + } + + async event (session: SessionData, event: Enriched, derived: boolean): Promise { + const result = await this.provideEvent(session, event, derived) + if (event.skipPropagate === true) { + return result + } + + await this.processDerived(session, [event], derived) + + return result + } + + async processDerived (session: SessionData, events: Enriched[], derived: boolean): Promise { + // Ensure asyncData is initialized + if (session.asyncData === undefined) { + session.asyncData = [] + } + + const triggerCtx: Omit = { + metadata: this.context.metadata, + client: this.context.client, + workspace: this.context.workspace, + account: session.account, + processedPeersEvents: this.processedPeersEvents, + derived, + execute: async (event: Event) => { + // Will be enriched in head + return (await this.context.head?.event(session, event as Enriched, true)) ?? {} + } + } + + if (!derived && session.isAsyncContext !== true && session.contextData !== undefined) { + session.isAsyncContext = true + const ctx = this.context.ctx.newChild('async-triggers', {}) + ctx.contextData = session.contextData + + this.callbacks.registerAsyncRequest(ctx, async (_ctx) => { + this.ctx = _ctx + await this.callAsyncTriggers({ ...triggerCtx, ctx: this.ctx }, session, events) + this.handleBroadcast( + session, + (session.asyncData as Enriched[]).sort((a, b) => a.date.getTime() - b.date.getTime()) + ) + session.asyncData = [] + }) + } else { + await this.callAsyncTriggers({ ...triggerCtx, ctx: this.ctx }, session, events) + + if (session.isAsyncContext !== true) { + this.handleBroadcast( + session, + (session.asyncData as Enriched[]).sort((a, b) => a.date.getTime() - b.date.getTime()) + ) + session.asyncData = [] + this.processedPeersEvents = new Set() + } + } + } + + private async callAsyncTriggers (ctx: TriggerCtx, session: SessionData, events: Enriched[]): Promise { + const fromTriggers = await this.runTriggers({ ...ctx, ctx: this.ctx }, events) + + for (const event of fromTriggers) { + await this.context.head?.event(session, event as Enriched, true) + } + + const triggersDerived = (fromTriggers as Enriched[]).filter((it) => it.skipPropagate !== true) + session.asyncData = [...session.asyncData, ...triggersDerived] + + await this.callAsyncNotifications(ctx, session, events) + } + + private async callAsyncNotifications ( + ctx: TriggerCtx, + session: SessionData, + events: Enriched[] + ): Promise { + const notifications = ( + await Promise.all( + events.map(async (event) => { + return await notify( + { + ...ctx, + ctx: this.ctx.newChild('create-notifications', {}) + }, + event + ) + }) + ) + ).flat() + await Promise.all(notifications.map((d) => this.context.head?.event(session, d as Enriched, true))) + const notificationsDerived = (notifications as Enriched[]).filter((it) => it.skipPropagate !== true) + session.asyncData = [...session.asyncData, ...notificationsDerived] + } + + private async runTriggers (ctx: TriggerCtx, events: Enriched[]): Promise { + return ( + await Promise.all( + events.map(async (event) => { + return await this.applyTriggers(event, ctx) + }) + ) + ).flat() + } + + private async applyTriggers (event: Enriched, ctx: Omit): Promise { + const matchedTriggers = triggers.filter(([_, type]) => type === event.type) + if (matchedTriggers.length === 0) return [] + + return ( + await Promise.all( + matchedTriggers.map(([name, _, fn]) => + fn( + { + ...ctx, + ctx: this.ctx.newChild(name, {}) + }, + event + ) + ) + ) + ).flat() + } +} diff --git a/foundations/communication/packages/server/src/middleware/validate.ts b/foundations/communication/packages/server/src/middleware/validate.ts new file mode 100644 index 0000000000..0107be0fd5 --- /dev/null +++ b/foundations/communication/packages/server/src/middleware/validate.ts @@ -0,0 +1,496 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type EventResult, + MessageEventType, + NotificationEventType, + type Event, + type SessionData, + PeerEventType +} from '@hcengineering/communication-sdk-types' +import { + type AccountUuid, + type BlobID, + type CardID, + type CardType, + type Collaborator, + type ContextID, + type FindCollaboratorsParams, + type FindLabelsParams, + type FindMessagesGroupParams, + type FindNotificationContextParams, + type FindNotificationsParams, + type Label, + type LabelID, + type MessageID, + type MessagesGroup, + type Notification, + type NotificationContext, + NotificationType, + SortingOrder +} from '@hcengineering/communication-types' +import { z, ZodString, ZodType, ZodTypeDef } from 'zod' +import { isBlobAttachmentType, isLinkPreviewAttachmentType } from '@hcengineering/communication-shared' + +import type { Enriched, Middleware, Subscription } from '../types' +import { BaseMiddleware } from './base' +import { ApiError } from '../error' + +export class ValidateMiddleware extends BaseMiddleware implements Middleware { + private validate(data: unknown, schema: z.ZodType): T { + const validationResult = schema.safeParse(data) + if (!validationResult.success) { + const errors = validationResult.error.errors.map((err) => err.message) + this.context.ctx.error(validationResult.error.message, data as any) + throw ApiError.badRequest(errors.join(', ')) + } + return validationResult.data + } + + async findMessagesGroups (session: SessionData, params: unknown): Promise { + const validParams: FindMessagesGroupParams = this.validate(params, FindMessagesGroupsParamsSchema) + return await this.provideFindMessagesGroups(session, validParams) + } + + async findNotificationContexts ( + session: SessionData, + params: unknown, + queryId?: Subscription + ): Promise { + const validParams: FindNotificationContextParams = this.validate(params, FindNotificationContextParamsSchema) + return await this.provideFindNotificationContexts(session, validParams, queryId) + } + + async findNotifications (session: SessionData, params: unknown, queryId?: Subscription): Promise { + const validParams: FindNotificationsParams = this.validate(params, FindNotificationsParamsSchema) + return await this.provideFindNotifications(session, validParams, queryId) + } + + async findLabels (session: SessionData, params: unknown, queryId?: Subscription): Promise { + const validParams: FindLabelsParams = this.validate(params, FindLabelsParamsSchema) + return await this.provideFindLabels(session, validParams, queryId) + } + + async findCollaborators (session: SessionData, params: unknown): Promise { + const validParams: FindCollaboratorsParams = this.validate(params, FindCollaboratorsParamsSchema) + return await this.provideFindCollaborators(session, validParams) + } + + async event (session: SessionData, event: Enriched, derived: boolean): Promise { + if (derived) return await this.provideEvent(session, event, derived) + switch (event.type) { + case MessageEventType.CreateMessage: + this.validate(event, CreateMessageEventSchema) + break + case MessageEventType.UpdatePatch: + this.validate(event, UpdatePatchEventSchema) + break + case MessageEventType.RemovePatch: + this.validate(event, RemovePatchEventSchema) + break + case MessageEventType.ReactionPatch: + this.validate(event, ReactionPatchEventSchema) + break + case MessageEventType.BlobPatch: + this.validate(event, BlobPatchEventSchema) + break + case MessageEventType.AttachmentPatch: + this.validate(event, AttachmentPatchEventSchema) + event.operations.forEach((op) => { + if (op.opcode === 'add' || op.opcode === 'set') { + op.attachments.forEach((att) => { + if (isLinkPreviewAttachmentType(att.mimeType)) { + this.validate(att.params, LinkPreviewParamsSchema) + } else if (isBlobAttachmentType(att.mimeType)) { + this.validate(att.params, BlobParamsSchema) + } + }) + } + }) + break + case MessageEventType.ThreadPatch: + this.validate(event, ThreadPatchEventSchema) + break + case NotificationEventType.AddCollaborators: + this.validate(event, AddCollaboratorsEventSchema) + break + case NotificationEventType.RemoveCollaborators: + this.validate(event, RemoveCollaboratorsEventSchema) + break + case NotificationEventType.UpdateNotification: + this.validate(event, UpdateNotificationsEventSchema) + break + case NotificationEventType.RemoveNotificationContext: + this.validate(event, RemoveNotificationContextEventSchema) + break + case NotificationEventType.UpdateNotificationContext: + this.validate(event, UpdateNotificationContextEventSchema) + break + case PeerEventType.CreatePeer: + this.validate(event, CreatePeerEventSchema) + break + case PeerEventType.RemovePeer: + this.validate(event, RemovePeerEventSchema) + break + } + return await this.provideEvent(session, deserializeEvent(event), derived) + } +} + +function brandedString (base: ZodString): ZodType { + return base.transform((s): Type => s as Type) +} + +const WorkspaceUuidSchema = z.string().uuid() +const AccountUuidSchema = brandedString(z.string()) +const BlobIDSchema = brandedString(z.string().uuid()) +const AttachmentIDSchema = z.string().uuid() +const CardIDSchema = brandedString(z.string()) +const CardTypeSchema = brandedString(z.string()) +const ContextIDSchema = brandedString(z.string()) +const DateSchema = z.coerce.date() +const LabelIDSchema = brandedString(z.string()) +const MarkdownSchema = z.string() +const MessageExtraSchema = z.any() +const MessageIDSchema = brandedString(z.string()) +const MessageTypeSchema = z.string() +const SocialIDSchema = z.string() +const SortingOrderSchema = z.union([z.literal(SortingOrder.Ascending), z.literal(SortingOrder.Descending)]) +const NotificationTypeSchema = z.nativeEnum(NotificationType) + +const BlobParamsSchema = z.object({ + blobId: BlobIDSchema, + mimeType: z.string(), + fileName: z.string(), + size: z.number(), + metadata: z.record(z.string(), z.any()).optional() +}) + +const LinkPreviewParamsSchema = z + .object({ + url: z.string(), + host: z.string(), + title: z.string().optional(), + description: z.string().optional(), + siteName: z.string().optional(), + iconUrl: z.string().optional(), + previewImage: z + .object({ + url: z.string(), + width: z.number().optional(), + height: z.number().optional() + }) + .optional() + }) + .strict() + +const UpdateBlobDataSchema = z.object({ + blobId: BlobIDSchema, + mimeType: z.string().optional(), + fileName: z.string().optional(), + size: z.number().optional(), + metadata: z.record(z.string(), z.any()).optional() +}) + +const AttachmentDataSchema = z.object({ + id: AttachmentIDSchema, + mimeType: z.string(), + params: z.record(z.string(), z.any()) +}) + +const AttachmentUpdateDataSchema = z.object({ + id: AttachmentIDSchema, + params: z.record(z.string(), z.any()) +}) + +// Find params +const DateOrRecordSchema = z.union([DateSchema, z.record(DateSchema)]) + +const FindParamsSchema = z + .object({ + order: SortingOrderSchema.optional(), + limit: z.number().optional() + }) + .strict() + +const FindNotificationContextParamsSchema = FindParamsSchema.extend({ + id: ContextIDSchema.optional(), + cardId: z.union([CardIDSchema, z.array(CardIDSchema)]).optional(), + lastNotify: DateOrRecordSchema.optional(), + account: z.union([AccountUuidSchema, z.array(AccountUuidSchema)]).optional(), + notifications: z + .object({ + type: NotificationTypeSchema.optional(), + limit: z.number(), + order: SortingOrderSchema, + read: z.boolean().optional(), + total: z.boolean().optional() + }) + .optional() +}).strict() + +const FindMessagesGroupsParamsSchema = FindParamsSchema.extend({ + cardId: CardIDSchema, + id: MessageIDSchema.optional(), + blobId: BlobIDSchema.optional(), + fromDate: DateOrRecordSchema.optional(), + toDate: DateOrRecordSchema.optional() +}).strict() + +const FindNotificationsParamsSchema = FindParamsSchema.extend({ + contextId: ContextIDSchema.optional(), + type: NotificationTypeSchema.optional(), + read: z.boolean().optional(), + created: DateOrRecordSchema.optional(), + account: z.union([AccountUuidSchema, z.array(AccountUuidSchema)]).optional(), + cardId: CardIDSchema.optional(), + total: z.boolean().optional() +}).strict() + +const FindLabelsParamsSchema = FindParamsSchema.extend({ + labelId: z.union([LabelIDSchema, z.array(LabelIDSchema)]).optional(), + cardId: CardIDSchema.optional(), + cardType: z.union([CardTypeSchema, z.array(CardTypeSchema)]).optional(), + account: AccountUuidSchema.optional() +}).strict() + +const FindCollaboratorsParamsSchema = FindParamsSchema.extend({ + cardId: CardIDSchema, + account: z.union([AccountUuidSchema, z.array(AccountUuidSchema)]).optional() +}).strict() + +// Events + +const BaseEventSchema = z + .object({ + _id: z.string().optional(), + _eventExtra: z.record(z.any()).optional() + }) + .strict() + +// Message events +const CreateMessageEventSchema = BaseEventSchema.extend({ + type: z.literal(MessageEventType.CreateMessage), + + cardId: CardIDSchema, + cardType: CardTypeSchema, + + messageId: brandedString(z.string().max(22)).optional(), + messageType: MessageTypeSchema, + + content: MarkdownSchema, + extra: MessageExtraSchema.optional(), + + socialId: SocialIDSchema, + date: DateSchema, + language: z.string().optional(), + + options: z + .object({ + skipLinkPreviews: z.boolean().optional(), + noNotify: z.boolean().optional(), + ignoreMentions: z.boolean().optional() + }) + .optional() +}).strict() + +const UpdatePatchEventSchema = BaseEventSchema.extend({ + type: z.literal(MessageEventType.UpdatePatch), + cardId: CardIDSchema, + messageId: MessageIDSchema.optional(), + + content: MarkdownSchema.optional(), + extra: z.record(z.any()).optional(), + language: z.string().optional(), + + socialId: SocialIDSchema, + date: DateSchema, + + options: z + .object({ + skipLinkPreviewsUpdate: z.boolean().optional(), + ignoreMentions: z.boolean().optional() + }) + .optional() +}).strict() + +const RemovePatchEventSchema = BaseEventSchema.extend({ + type: z.literal(MessageEventType.RemovePatch), + cardId: CardIDSchema, + messageId: MessageIDSchema.optional(), + + socialId: SocialIDSchema, + date: DateSchema +}).strict() + +const ReactionOperationSchema = z.union([ + z.object({ opcode: z.literal('add'), reaction: z.string() }), + z.object({ opcode: z.literal('remove'), reaction: z.string() }) +]) + +const ReactionPatchEventSchema = BaseEventSchema.extend({ + type: z.literal(MessageEventType.ReactionPatch), + cardId: CardIDSchema, + messageId: MessageIDSchema, + operation: ReactionOperationSchema, + personUuid: z.string(), + socialId: SocialIDSchema, + date: DateSchema +}).strict() + +/** + * @deprecated + */ +const BlobOperationSchema = z.union([ + z.object({ opcode: z.literal('attach'), blobs: z.array(BlobParamsSchema).nonempty() }), + z.object({ opcode: z.literal('detach'), blobIds: z.array(BlobIDSchema).nonempty() }), + z.object({ opcode: z.literal('set'), blobs: z.array(BlobParamsSchema).nonempty() }), + z.object({ opcode: z.literal('update'), blobs: z.array(UpdateBlobDataSchema).nonempty() }) +]) + +/** + * @deprecated + */ +const BlobPatchEventSchema = BaseEventSchema.extend({ + type: z.literal(MessageEventType.BlobPatch), + cardId: CardIDSchema, + messageId: MessageIDSchema, + operations: z.array(BlobOperationSchema).nonempty(), + socialId: SocialIDSchema, + date: DateSchema +}).strict() + +const AttachmentOperationSchema = z.union([ + z.object({ opcode: z.literal('add'), attachments: z.array(AttachmentDataSchema).nonempty() }), + z.object({ opcode: z.literal('remove'), ids: z.array(AttachmentIDSchema).nonempty() }), + z.object({ opcode: z.literal('set'), attachments: z.array(AttachmentDataSchema).nonempty() }), + z.object({ opcode: z.literal('update'), attachments: z.array(AttachmentUpdateDataSchema).nonempty() }) +]) + +const AttachmentPatchEventSchema = BaseEventSchema.extend({ + type: z.literal(MessageEventType.AttachmentPatch), + cardId: CardIDSchema, + messageId: MessageIDSchema, + operations: z.array(AttachmentOperationSchema).nonempty(), + socialId: SocialIDSchema, + date: DateSchema +}).strict() + +const ThreadPatchEventSchema = BaseEventSchema.extend({ + type: z.literal(MessageEventType.ThreadPatch), + cardId: CardIDSchema, + messageId: MessageIDSchema, + operation: z.object({ opcode: z.literal('attach'), threadId: CardIDSchema, threadType: CardTypeSchema }), + socialId: SocialIDSchema, + personUuid: z.string(), + date: DateSchema +}).strict() + +// Notification events +const UpdateNotificationsEventSchema = BaseEventSchema.extend({ + type: z.literal(NotificationEventType.UpdateNotification), + contextId: ContextIDSchema, + account: AccountUuidSchema, + query: z.object({ + id: z.string().optional(), + type: z.string().optional(), + untilDate: DateSchema.optional() + }), + updates: z.object({ + read: z.boolean() + }), + date: DateSchema +}).strict() + +const RemoveNotificationContextEventSchema = BaseEventSchema.extend({ + type: z.literal(NotificationEventType.RemoveNotificationContext), + contextId: ContextIDSchema, + account: AccountUuidSchema, + date: DateSchema +}).strict() + +const UpdateNotificationContextEventSchema = BaseEventSchema.extend({ + type: z.literal(NotificationEventType.UpdateNotificationContext), + contextId: ContextIDSchema, + account: AccountUuidSchema, + updates: z.object({ + lastView: DateSchema.optional() + }), + date: DateSchema +}).strict() + +const AddCollaboratorsEventSchema = BaseEventSchema.extend({ + type: z.literal(NotificationEventType.AddCollaborators), + cardId: CardIDSchema, + cardType: CardTypeSchema, + collaborators: z.array(AccountUuidSchema).nonempty(), + socialId: SocialIDSchema, + date: DateSchema +}).strict() + +const RemoveCollaboratorsEventSchema = BaseEventSchema.extend({ + type: z.literal(NotificationEventType.RemoveCollaborators), + cardId: CardIDSchema, + cardType: CardTypeSchema, + collaborators: z.array(AccountUuidSchema).nonempty(), + socialId: SocialIDSchema, + date: DateSchema +}).strict() + +const CreatePeerEventSchema = BaseEventSchema.extend({ + type: z.literal(PeerEventType.CreatePeer), + workspaceId: WorkspaceUuidSchema, + cardId: CardIDSchema, + kind: z.string().nonempty(), + value: z.string().nonempty(), + extra: z.record(z.any()).optional(), + options: z + .object({ + newValue: z.boolean().optional() + }) + .optional(), + date: DateSchema +}).strict() + +const RemovePeerEventSchema = BaseEventSchema.extend({ + type: z.literal(PeerEventType.RemovePeer), + workspaceId: WorkspaceUuidSchema, + cardId: CardIDSchema, + kind: z.string().nonempty(), + value: z.string().nonempty(), + date: DateSchema +}).strict() + +function deserializeEvent (event: Enriched): Enriched { + switch (event.type) { + case NotificationEventType.UpdateNotificationContext: + event.updates.lastView = deserializeDate(event.updates.lastView) + break + case NotificationEventType.UpdateNotification: + event.query.untilDate = deserializeDate(event.query.untilDate) + break + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + event.date = deserializeDate(event.date)! + return event +} + +function deserializeDate (date?: Date | string | undefined | null): Date | undefined { + if (date == null) return undefined + if (date instanceof Date) return date + return new Date(date) +} diff --git a/foundations/communication/packages/server/src/middlewares.ts b/foundations/communication/packages/server/src/middlewares.ts new file mode 100644 index 0000000000..ec6a66e45f --- /dev/null +++ b/foundations/communication/packages/server/src/middlewares.ts @@ -0,0 +1,212 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MeasureContext } from '@hcengineering/core' +import type { EventResult, Event, SessionData } from '@hcengineering/communication-sdk-types' +import type { + CardID, + Collaborator, + FindCollaboratorsParams, + FindLabelsParams, FindMessagesGroupParams, FindMessagesMetaParams, + FindNotificationContextParams, + FindNotificationsParams, + FindPeersParams, + Label, MessageMeta, MessagesGroup, + Notification, + NotificationContext, Peer, + WorkspaceUuid +} from '@hcengineering/communication-types' + +import type { + CommunicationCallbacks, + Enriched, + Metadata, + Middleware, + MiddlewareContext, + MiddlewareCreateFn, + Subscription +} from './types' +import { PermissionsMiddleware } from './middleware/permissions' +import { StorageMiddleware } from './middleware/storage' +import { BroadcastMiddleware } from './middleware/broadcast' +import { TriggersMiddleware } from './middleware/triggers' +import { ValidateMiddleware } from './middleware/validate' +import { DateMiddleware } from './middleware/date' +import { IdentityMiddleware } from './middleware/indentity' +import { IdMiddleware } from './middleware/id' +import { PeerMiddleware } from './middleware/peer' +import { LowLevelClient } from './client' + +export async function buildMiddlewares ( + ctx: MeasureContext, + workspace: WorkspaceUuid, + metadata: Metadata, + client: LowLevelClient, + callbacks: CommunicationCallbacks +): Promise { + const peers = await client.db.findPeers({ workspaceId: workspace }) + + const createFns: MiddlewareCreateFn[] = [ + // Enrich events + async (context, next) => new DateMiddleware(context, next), + async (context, next) => new IdentityMiddleware(context, next), + async (context, next) => new IdMiddleware(context, next), + + // Validate events + async (context, next) => new ValidateMiddleware(context, next), + async (context, next) => new PermissionsMiddleware(context, next), + + // Process events + async (context, next) => new TriggersMiddleware(callbacks, context, next), + async (context, next) => new BroadcastMiddleware(callbacks, context, next), + async (context, next) => new StorageMiddleware(context, next), + async (context, next) => new PeerMiddleware(context, next) + ] + + const context: MiddlewareContext = { + ctx, + metadata, + workspace, + client, + cadsWithPeers: new Set(peers.map(it => it.cardId)) + } + + return await Middlewares.create(ctx, context, createFns) +} + +export class Middlewares { + private head: Middleware | undefined + + private readonly middlewares: Middleware[] = [] + + private constructor ( + private readonly ctx: MeasureContext, + private readonly context: MiddlewareContext + ) { + } + + static async create ( + ctx: MeasureContext, + context: MiddlewareContext, + createFns: MiddlewareCreateFn[] + ): Promise { + const middlewares = new Middlewares(ctx, context) + + const head = await middlewares.buildChain(ctx, createFns, middlewares.context) + middlewares.head = head + context.head = head + return middlewares + } + + private async buildChain ( + ctx: MeasureContext, + createFns: MiddlewareCreateFn[], + context: MiddlewareContext + ): Promise { + let current: Middleware | undefined + for (let index = createFns.length - 1; index >= 0; index--) { + const createFn = createFns[index] + try { + const nextCurrent = await createFn(context, current) + this.middlewares.push(nextCurrent) + current = nextCurrent + } catch (err: any) { + ctx.error('failed to initialize middlewares', { err, workspace: context.workspace }) + await this.close() + throw err + } + } + this.middlewares.reverse() + + return current + } + + async findMessagesGroups (session: SessionData, params: FindMessagesGroupParams): Promise { + if (this.head === undefined) return [] + return await this.head.findMessagesGroups(session, params) + } + + async findMessagesMeta (session: SessionData, params: FindMessagesMetaParams): Promise { + if (this.head === undefined) return [] + return await this.head.findMessagesMeta(session, params) + } + + async findNotificationContexts ( + session: SessionData, + params: FindNotificationContextParams, + queryId?: Subscription + ): Promise { + if (this.head === undefined) return [] + return await this.head.findNotificationContexts(session, params, queryId) + } + + async findNotifications ( + session: SessionData, + params: FindNotificationsParams, + queryId?: Subscription + ): Promise { + if (this.head === undefined) return [] + return await this.head.findNotifications(session, params, queryId) + } + + async findLabels (session: SessionData, params: FindLabelsParams): Promise { + if (this.head === undefined) return [] + return await this.head.findLabels(session, params) + } + + async findCollaborators (session: SessionData, params: FindCollaboratorsParams): Promise { + if (this.head === undefined) return [] + return await this.head.findCollaborators(session, params) + } + + async findPeers (session: SessionData, params: FindPeersParams): Promise { + if (this.head === undefined) return [] + return await this.head.findPeers(session, params) + } + + subscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { + if (this.head === undefined) return + this.head?.subscribeCard(session, cardId, subscription) + } + + unsubscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { + if (this.head === undefined) return + this.head?.unsubscribeCard(session, cardId, subscription) + } + + async event (session: SessionData, event: Event): Promise { + if (this.head === undefined) return {} + const result = (await this.head?.event(session, event as Enriched, session.derived ?? false)) ?? {} + + this.head?.handleBroadcast(session, [event] as Enriched[]) + + return result + } + + async closeSession (sessionId: string): Promise { + if (this.head === undefined) return + this.head.closeSession(sessionId) + } + + async close (): Promise { + for (const mw of this.middlewares) { + try { + mw.close() + } catch (err: any) { + this.ctx.error('Failed to close middleware', { err, workspace: this.context.workspace }) + } + } + } +} diff --git a/foundations/communication/packages/server/src/notification/notification.ts b/foundations/communication/packages/server/src/notification/notification.ts new file mode 100644 index 0000000000..bfa83d18d5 --- /dev/null +++ b/foundations/communication/packages/server/src/notification/notification.ts @@ -0,0 +1,405 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type CreateNotificationContextResult, + NotificationEventType, + type Event, + MessageEventType +} from '@hcengineering/communication-sdk-types' +import { + type AccountUuid, + BlobID, + type CardID, + type CardType, + type ContextID, + Markdown, + type MessageID, + type NotificationContext, + NotificationType, + type ReactionNotificationContent, + type SocialID, + SortingOrder +} from '@hcengineering/communication-types' +import { markdownToMarkup } from '@hcengineering/text-markdown' +import { jsonToMarkup, markupToText } from '@hcengineering/text-core' +import { readOnlyGuestAccountUuid } from '@hcengineering/core' + +import type { Enriched, TriggerCtx } from '../types' +import { getNameBySocialID } from '../triggers/utils' + +const BATCH_SIZE = 500 +const maxDate = new Date('9999-12-31T23:59:59Z') + +export async function notify (ctx: TriggerCtx, event: Enriched): Promise { + switch (event.type) { + case MessageEventType.CreateMessage: { + if (event.options?.noNotify === true || event.messageId == null) return [] + const meta = await ctx.client.getMessageMeta(event.cardId, event.messageId) + if (meta == null) return [] + return await notifyMessage( + ctx, + event.cardId, + event.cardType, + event.messageId, + meta.blobId, + event.content, + event.socialId, + event.date + ) + } + case MessageEventType.ReactionPatch: { + if (event.operation.opcode === 'add') { + return await notifyReaction( + ctx, + event.cardId, + event.messageId, + event.operation.reaction, + event.socialId, + event.date + ) + } else if (event.operation.opcode === 'remove') { + return await removeReactionNotification( + ctx, + event.cardId, + event.messageId, + event.operation.reaction, + event.socialId + ) + } + } + } + + return [] +} + +async function removeReactionNotification ( + ctx: TriggerCtx, + cardId: CardID, + messageId: MessageID, + reaction: string, + socialId: SocialID +): Promise { + const result: Event[] = [] + const meta = await ctx.client.getMessageMeta(cardId, messageId) + if (meta == null) return result + + const messageAccount = (await ctx.client.findPersonUuid( + { + ctx: ctx.ctx, + account: ctx.account + }, + meta.creator + )) as AccountUuid | undefined + if (messageAccount == null) return result + + const notifications = await ctx.client.db.findNotifications({ + type: NotificationType.Reaction, + messageId, + account: messageAccount + }) + + const toDelete = notifications.find((n) => { + const content = n.content as ReactionNotificationContent + return content.emoji === reaction && n.creator === socialId + }) + + if (toDelete === undefined) return result + + const context = (await ctx.client.db.findNotificationContexts({ cardId, account: messageAccount, limit: 1 }))[0] + if (context == null) return result + if (context.lastNotify != null && context.lastNotify.getTime() === toDelete.created.getTime()) { + const lastNotification = ( + await ctx.client.db.findNotifications({ + account: messageAccount, + contextId: context.id, + created: { + less: context.lastNotify + }, + order: SortingOrder.Descending, + limit: 1 + }) + )[0] + if (lastNotification != null) { + result.push({ + type: NotificationEventType.UpdateNotificationContext, + contextId: context.id, + account: messageAccount, + updates: { + lastNotify: lastNotification.created + }, + date: new Date() + }) + } + } + + result.push({ + type: NotificationEventType.RemoveNotifications, + contextId: toDelete.contextId, + account: messageAccount, + ids: [toDelete.id] + }) + + return result +} +async function notifyReaction ( + ctx: TriggerCtx, + cardId: CardID, + messageId: MessageID, + reaction: string, + socialId: SocialID, + date: Date +): Promise { + const result: Event[] = [] + + const meta = await ctx.client.getMessageMeta(cardId, messageId) + if (meta == null) return result + + const messageAccount = (await ctx.client.findPersonUuid(ctx, meta.creator, true)) as AccountUuid | undefined + if (messageAccount == null) return result + + const spaceMembers = await ctx.client.db.getCardSpaceMembers(cardId) + if (!spaceMembers.includes(messageAccount)) return [] + + const reactionAccount = (await ctx.client.findPersonUuid(ctx, socialId, true)) as AccountUuid | undefined + if (reactionAccount === messageAccount) return result + + const context = (await ctx.client.db.findNotificationContexts({ cardId, account: messageAccount }))[0] + let contextId: ContextID | undefined = context?.id + + if (context == null) { + contextId = await createContext(ctx, messageAccount, cardId, date, undefined, date) + } + + if (contextId == null) return result + + const content: ReactionNotificationContent = { + emoji: reaction, + senderName: (await getNameBySocialID(ctx, socialId)) ?? 'System', + title: 'Reacted to your message', + shortText: reaction + } + result.push({ + type: NotificationEventType.CreateNotification, + notificationType: NotificationType.Reaction, + account: messageAccount, + cardId, + contextId, + messageId, + blobId: meta.blobId, + date, + content, + creator: socialId, + read: messageAccount === readOnlyGuestAccountUuid + }) + + if ((context?.lastNotify?.getTime() ?? date.getTime()) < date.getTime()) { + result.push({ + type: NotificationEventType.UpdateNotificationContext, + contextId, + account: messageAccount, + updates: { + lastNotify: date + }, + date + }) + } + return result +} + +async function notifyMessage ( + ctx: TriggerCtx, + cardId: CardID, + cardType: CardType, + messageId: MessageID, + blobId: BlobID, + markdown: Markdown, + socialId: SocialID, + date: Date +): Promise { + const { client } = ctx + const cursor = client.db.getCollaboratorsCursor(cardId, date, BATCH_SIZE) + const spaceMembers = await client.db.getCardSpaceMembers(cardId) + const creatorAccount = (await ctx.client.findPersonUuid(ctx, socialId, true)) as AccountUuid | undefined + const result: Event[] = [] + + const cardTitle = (await ctx.client.db.getCardTitle(cardId)) ?? 'New message' + + let isFirstBatch = true + + for await (const dbCollaborators of cursor) { + const collaborators: AccountUuid[] = dbCollaborators.map((it) => it.account) + const contexts: NotificationContext[] = await client.db.findNotificationContexts({ + cardId, + account: isFirstBatch && collaborators.length < BATCH_SIZE ? undefined : collaborators + }) + + for (const collaborator of collaborators) { + if (!spaceMembers.includes(collaborator)) continue + try { + const context = contexts.find((it) => it.account === collaborator) + const res = await processCollaborator( + ctx, + cardId, + cardType, + cardTitle, + messageId, + blobId, + markdown, + date, + collaborator, + socialId, + creatorAccount, + context + ) + result.push(...res) + } catch (e) { + ctx.ctx.error('Error on create notification', { collaborator, error: e }) + } + } + + isFirstBatch = false + } + + return result +} + +async function processCollaborator ( + ctx: TriggerCtx, + cardId: CardID, + cardType: CardType, + cardTitle: string, + messageId: MessageID, + blobId: BlobID, + markdown: Markdown, + date: Date, + collaborator: AccountUuid, + creatorSocialId: SocialID, + creatorAccount?: AccountUuid, + context?: NotificationContext +): Promise { + const result: Event[] = [] + const isOwn = creatorAccount === collaborator + const { contextId, events } = await createOrUpdateContext(ctx, cardId, date, collaborator, isOwn, context) + + result.push(...events) + + if (contextId == null || isOwn) return result + + const text = markupToText(jsonToMarkup(markdownToMarkup(markdown))) + const shortText = text.slice(0, 100) + const isRead = collaborator === readOnlyGuestAccountUuid + result.push({ + type: NotificationEventType.CreateNotification, + notificationType: NotificationType.Message, + account: collaborator, + contextId, + cardId, + messageId, + blobId, + date, + creator: creatorSocialId, + content: { + senderName: (await getNameBySocialID(ctx, creatorSocialId)) ?? 'System', + title: cardTitle, + shortText: shortText.length < text.length ? shortText + '...' : text + }, + read: isRead || date.getTime() < (context?.lastView?.getTime() ?? 0) + }) + return result +} + +async function createOrUpdateContext ( + ctx: TriggerCtx, + cardId: CardID, + date: Date, + collaborator: AccountUuid, + isOwn: boolean, + context?: NotificationContext +): Promise<{ + contextId: ContextID | undefined + events: Event[] + }> { + if (context == null) { + const lastView = collaborator === readOnlyGuestAccountUuid ? maxDate : isOwn ? date : undefined + const contextId = await createContext(ctx, collaborator, cardId, date, lastView, date) + + return { + contextId, + events: [] + } + } + + const lastUpdate = context.lastUpdate == null || date > context.lastUpdate ? date : context.lastUpdate + const lastView = + collaborator === readOnlyGuestAccountUuid ? maxDate : isOwn && isContextRead(context) ? date : undefined + + return { + contextId: context.id, + events: [ + { + type: NotificationEventType.UpdateNotificationContext, + contextId: context.id, + account: collaborator, + updates: { + lastView, + lastUpdate, + lastNotify: isOwn ? undefined : date + }, + date: new Date() + } + ] + } +} + +async function createContext ( + ctx: TriggerCtx, + account: AccountUuid, + cardId: CardID, + lastUpdate: Date, + lastView: Date | undefined, + lastNotify: Date +): Promise { + try { + const result = (await ctx.execute({ + type: NotificationEventType.CreateNotificationContext, + account, + cardId, + lastUpdate, + lastView: lastView ?? new Date(lastUpdate.getTime() - 1), + lastNotify, + date: new Date() + })) as CreateNotificationContextResult + + return result.id + } catch (e) { + return ( + await ctx.client.db.findNotificationContexts({ + account, + cardId + }) + )[0]?.id + } +} + +function isContextRead (context: NotificationContext): boolean { + const { lastView, lastUpdate } = context + + if (lastView == null) { + return false + } + + return lastView >= lastUpdate +} diff --git a/foundations/communication/packages/server/src/triggers/all.ts b/foundations/communication/packages/server/src/triggers/all.ts new file mode 100644 index 0000000000..3ea8496dfa --- /dev/null +++ b/foundations/communication/packages/server/src/triggers/all.ts @@ -0,0 +1,23 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Triggers } from '../types' +import message from './message' +import notification from './notification' +import card from './card' + +const allTriggers: Triggers = [...message, ...notification, ...card] + +export default allTriggers diff --git a/foundations/communication/packages/server/src/triggers/card.ts b/foundations/communication/packages/server/src/triggers/card.ts new file mode 100644 index 0000000000..7affc95382 --- /dev/null +++ b/foundations/communication/packages/server/src/triggers/card.ts @@ -0,0 +1,124 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + CardEventType, + MessageEventType, + NotificationEventType, + type Event, + UpdateCardTypeEvent, + RemoveCardEvent +} from '@hcengineering/communication-sdk-types' +import { type ActivityTypeUpdate, ActivityUpdateType, MessageType } from '@hcengineering/communication-types' + +import type { Enriched, TriggerCtx, TriggerFn, Triggers } from '../types' + +async function createActivityOnCardTypeUpdate (ctx: TriggerCtx, event: UpdateCardTypeEvent): Promise { + const updateDate: ActivityTypeUpdate = { + type: ActivityUpdateType.Type, + newType: event.cardType + } + + return [ + { + type: MessageEventType.CreateMessage, + messageType: MessageType.Activity, + cardId: event.cardId, + cardType: event.cardType, + content: 'Changed type', + socialId: event.socialId, + date: event.date, + extra: { + action: 'update', + update: updateDate + } + } + ] +} + +async function onCardTypeUpdates (ctx: TriggerCtx, event: Enriched): Promise { + await ctx.client.db.updateCollaborators({ cardId: event.cardId }, { cardType: event.cardType }) + await ctx.client.db.updateLabels({ cardId: event.cardId }, { cardType: event.cardType }) + + const thread = (await ctx.client.db.findThreadMeta({ threadId: event.cardId, limit: 1 }))[0] + if (thread === undefined) return [] + + return [ + { + type: MessageEventType.ThreadPatch, + cardId: thread.cardId, + messageId: thread.messageId, + operation: { + opcode: 'update', + threadId: thread.threadId, + update: { + threadType: event.cardType + } + }, + socialId: event.socialId, + date: event.date + } + ] +} + +async function removeCardCollaborators (ctx: TriggerCtx, event: UpdateCardTypeEvent): Promise { + await ctx.client.db.removeCollaborators({ cardId: event.cardId }) + return [] +} + +async function removeCardLabels (ctx: TriggerCtx, event: UpdateCardTypeEvent): Promise { + await ctx.client.db.removeLabels({ cardId: event.cardId }) + return [] +} + +async function removeCardThreads (ctx: TriggerCtx, event: RemoveCardEvent): Promise { + const toRemove = await ctx.client.db.findThreadMeta({ threadId: event.cardId }) + + await ctx.client.db.removeThreadMeta({ cardId: event.cardId }) + await ctx.client.db.removeThreadMeta({ threadId: event.cardId }) + + for (const thread of toRemove) { + const meta = await ctx.client.getMessageMeta(thread.cardId, thread.messageId) + if (meta === undefined) continue + await ctx.client.blob.removeThread(thread.cardId, meta.blobId, thread.messageId, thread.threadId) + } + + return [] +} + +async function removeNotificationContexts (ctx: TriggerCtx, event: RemoveCardEvent): Promise { + const result: Event[] = [] + const contexts = await ctx.client.db.findNotificationContexts({ cardId: event.cardId }) + for (const context of contexts) { + result.push({ + type: NotificationEventType.RemoveNotificationContext, + contextId: context.id, + account: context.account, + date: new Date() + }) + } + return result +} + +const triggers: Triggers = [ + ['on_card_type_updates', CardEventType.UpdateCardType, onCardTypeUpdates as TriggerFn], + ['create_activity_on_card_type_updates', CardEventType.UpdateCardType, createActivityOnCardTypeUpdate as TriggerFn], + ['remove_collaborators_on_card_removed', CardEventType.RemoveCard, removeCardCollaborators as TriggerFn], + ['remove_labels_on_card_removed', CardEventType.RemoveCard, removeCardLabels as TriggerFn], + ['remove_threads_on_card_removed', CardEventType.RemoveCard, removeCardThreads as TriggerFn], + ['remove_notification_contexts_on_card_removed', CardEventType.RemoveCard, removeNotificationContexts as TriggerFn] +] + +export default triggers diff --git a/foundations/communication/packages/server/src/triggers/message.ts b/foundations/communication/packages/server/src/triggers/message.ts new file mode 100644 index 0000000000..2cdccaacde --- /dev/null +++ b/foundations/communication/packages/server/src/triggers/message.ts @@ -0,0 +1,226 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + CreateMessageEvent, + type Event, + MessageEventType, + NotificationEventType, + PatchEvent, + RemovePatchEvent, + ThreadPatchEvent +} from '@hcengineering/communication-sdk-types' +import { CardPeer, MessageType, Peer } from '@hcengineering/communication-types' +import { type AccountUuid, generateId } from '@hcengineering/core' +import { extractReferences } from '@hcengineering/text-core' +import { markdownToMarkup } from '@hcengineering/text-markdown' + +import type { Enriched, TriggerCtx, TriggerFn, Triggers } from '../types' +import { generateMessageId } from '../messageId' + +async function addCollaborators (ctx: TriggerCtx, event: Enriched): Promise { + const { messageType, socialId, content, cardId, cardType, date } = event + if (messageType === MessageType.Activity) return [] + const account = (await ctx.client.findPersonUuid(ctx, socialId, true)) as AccountUuid | undefined + const collaborators = new Set() + + if (account !== undefined) { + collaborators.add(account) + } + + if (event.options?.ignoreMentions !== true) { + const markup = markdownToMarkup(content) + const references = extractReferences(markup) + const personIds = references + .filter((it) => ['contact:class:Person', 'contact:mixin:Employee'].includes(it.objectClass)) + .map((it) => it.objectId) + .filter((it) => it != null) as string[] + const accounts = await ctx.client.db.getAccountsByPersonIds(personIds) + + if (accounts.length > 0) { + const spaceMembers = await ctx.client.db.getCardSpaceMembers(cardId) + for (const account of accounts) { + if (spaceMembers.includes(account)) { + collaborators.add(account) + } + } + } + } + + if (collaborators.size === 0) { + return [] + } + + return [ + { + type: NotificationEventType.AddCollaborators, + cardId, + cardType, + collaborators: Array.from(collaborators), + socialId, + date: new Date(date.getTime() - 1) + } + ] +} + +async function addThreadReply (ctx: TriggerCtx, event: Enriched): Promise { + if (event.messageType !== MessageType.Text || event.extra?.threadRoot === true) { + return [] + } + const { cardId, socialId, date } = event + const thread = (await ctx.client.db.findThreadMeta({ threadId: cardId, limit: 1 }))[0] + + if (thread === undefined) return [] + + return [ + { + type: MessageEventType.ThreadPatch, + cardId: thread.cardId, + messageId: thread.messageId, + operation: { + opcode: 'addReply', + threadId: thread.threadId + }, + socialId, + date + } + ] +} + +async function removeThreadReply (ctx: TriggerCtx, event: Enriched): Promise { + const { cardId } = event + const thread = (await ctx.client.db.findThreadMeta({ threadId: cardId, limit: 1 }))[0] + if (thread === undefined) return [] + + return [ + { + type: MessageEventType.ThreadPatch, + cardId: thread.cardId, + messageId: thread.messageId, + operation: { + opcode: 'removeReply', + threadId: thread.threadId + }, + date: event.date, + socialId: event.socialId + } + ] +} + +async function onThreadAttached (ctx: TriggerCtx, event: Enriched): Promise { + if (event.operation.opcode !== 'attach') return [] + const message = await ctx.client.findMessage(event.cardId, event.messageId, { attachments: true }) + + if (message === undefined) return [] + + const result: Event[] = [] + + if (message.type === MessageType.Activity || message.extra?.threadRoot === true) { + return [] + } + + const messageId = generateMessageId() + + result.push({ + messageId, + type: MessageEventType.CreateMessage, + messageType: message.type, + cardId: event.operation.threadId, + cardType: event.operation.threadType, + content: message.content, + extra: { ...message.extra, threadRoot: true }, + socialId: message.creator, + date: message.created, + options: { + noNotify: true + } + }) + + result.push({ + type: MessageEventType.AttachmentPatch, + cardId: event.operation.threadId, + messageId, + operations: [ + { + opcode: 'add', + attachments: message.attachments.map((it) => ({ + id: it.id, + mimeType: it.mimeType, + params: it.params + })) + } + ], + socialId: message.creator, + date: message.created + }) + + return result +} + +async function checkPeers (ctx: TriggerCtx, event: Enriched): Promise { + if (ctx.processedPeersEvents.has(event._id)) return [] + if (event.type === MessageEventType.CreateMessage) { + if (event.messageType === MessageType.Activity) { + return [] + } + } + + if (event.type === MessageEventType.ThreadPatch) { + return [] + } + + const cardPeers = new Set( + (((event._eventExtra.peers ?? []) as Peer[]).filter((it) => it.kind === 'card') as CardPeer[]) + .flatMap((it) => it.members) + .filter((it) => it.workspaceId === ctx.workspace && it.cardId !== event.cardId) + .map((it) => it.cardId) + ) + + if (cardPeers.size === 0) return [] + const res: Event[] = [] + + for (const peer of cardPeers) { + const ev = { + ...event, + _id: generateId(), + cardId: peer + } + + ctx.processedPeersEvents.add(ev._id) + + res.push(ev) + } + + return res +} + +const triggers: Triggers = [ + ['add_collaborators_on_message_created', MessageEventType.CreateMessage, addCollaborators as TriggerFn], + + ['add_thread_reply_on_message_created', MessageEventType.CreateMessage, addThreadReply as TriggerFn], + ['remove_reply_on_messages_removed', MessageEventType.RemovePatch, removeThreadReply as TriggerFn], + + ['on_thread_created', MessageEventType.ThreadPatch, onThreadAttached as TriggerFn], + + ['check_peers_on_message_created', MessageEventType.CreateMessage, checkPeers as TriggerFn], + ['check_peers_on_update_patch', MessageEventType.UpdatePatch, checkPeers as TriggerFn], + ['check_peers_on_remove_patch', MessageEventType.RemovePatch, checkPeers as TriggerFn], + ['check_peers_on_reaction_patch', MessageEventType.ReactionPatch, checkPeers as TriggerFn], + ['check_peers_on_blob_patch', MessageEventType.BlobPatch, checkPeers as TriggerFn], + ['check_peers_on_attachment_patch', MessageEventType.AttachmentPatch, checkPeers as TriggerFn], + ['check_peers_on_thread_patch', MessageEventType.ThreadPatch, checkPeers as TriggerFn] +] + +export default triggers diff --git a/foundations/communication/packages/server/src/triggers/notification.ts b/foundations/communication/packages/server/src/triggers/notification.ts new file mode 100644 index 0000000000..8159f7a746 --- /dev/null +++ b/foundations/communication/packages/server/src/triggers/notification.ts @@ -0,0 +1,189 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + AddCollaboratorsEvent, + LabelEventType, + MessageEventType, + NotificationEventType, + type Event, + UpdateNotificationContextEvent, + RemovePatchEvent, + RemoveCollaboratorsEvent +} from '@hcengineering/communication-sdk-types' +import { + type AccountUuid, + type ActivityCollaboratorsUpdate, + ActivityUpdateType, + MessageType, + NotificationType, + SubscriptionLabelID +} from '@hcengineering/communication-types' +import { groupByArray } from '@hcengineering/core' + +import type { TriggerCtx, TriggerFn, Triggers } from '../types' +import { getAddCollaboratorsMessageContent, getRemoveCollaboratorsMessageContent } from './utils' + +async function onAddedCollaborators (ctx: TriggerCtx, event: AddCollaboratorsEvent): Promise { + const { cardId, cardType, collaborators } = event + + if (collaborators.length === 0) return [] + const result: Event[] = [] + + for (const collaborator of collaborators) { + result.push({ + type: LabelEventType.CreateLabel, + cardId, + cardType, + account: collaborator, + labelId: SubscriptionLabelID, + date: event.date + }) + } + + const account = (await ctx.client.findPersonUuid(ctx, event.socialId, true)) as AccountUuid | undefined + + const updateDate: ActivityCollaboratorsUpdate = { + type: ActivityUpdateType.Collaborators, + added: collaborators, + removed: [] + } + result.push({ + type: MessageEventType.CreateMessage, + messageType: MessageType.Activity, + cardId, + cardType, + content: await getAddCollaboratorsMessageContent(ctx, account, collaborators), + socialId: event.socialId, + date: event.date, + extra: { + action: 'update', + update: updateDate + } + }) + return result +} + +async function onRemovedCollaborators (ctx: TriggerCtx, event: RemoveCollaboratorsEvent): Promise { + const { cardId, collaborators } = event + if (collaborators.length === 0) return [] + const result: Event[] = [] + const contexts = await ctx.client.db.findNotificationContexts({ cardId, account: event.collaborators }) + for (const collaborator of collaborators) { + const context = contexts.find((it) => it.account === collaborator) + result.push({ + type: LabelEventType.RemoveLabel, + cardId, + account: collaborator, + labelId: SubscriptionLabelID, + date: event.date + }) + + if (context !== undefined && context.lastUpdate.getTime() > context.lastView.getTime()) { + result.push({ + type: NotificationEventType.UpdateNotificationContext, + contextId: context.id, + account: collaborator, + updates: { + lastView: context.lastUpdate + }, + date: new Date() + }) + } + } + + const updateDate: ActivityCollaboratorsUpdate = { + type: ActivityUpdateType.Collaborators, + added: [], + removed: collaborators + } + const account = (await ctx.client.findPersonUuid(ctx, event.socialId, true)) as AccountUuid | undefined + result.push({ + type: MessageEventType.CreateMessage, + messageType: MessageType.Activity, + cardId, + cardType: event.cardType, + content: await getRemoveCollaboratorsMessageContent(ctx, account, collaborators), + socialId: event.socialId, + date: event.date, + extra: { + action: 'update', + update: updateDate + } + }) + + return result +} + +async function onNotificationContextUpdated (ctx: TriggerCtx, event: UpdateNotificationContextEvent): Promise { + const { contextId, updates } = event + const { lastView } = updates + if (lastView == null) return [] + + const context = (await ctx.client.db.findNotificationContexts({ id: contextId }))[0] + if (context == null) return [] + const result: Event[] = [] + + result.push({ + type: NotificationEventType.UpdateNotification, + account: context.account, + contextId: context.id, + query: { + type: NotificationType.Message, + untilDate: context.lastView + }, + updates: { + read: true + } + }) + + return result +} + +async function onMessagesRemoved (ctx: TriggerCtx, event: RemovePatchEvent): Promise { + const notifications = await ctx.client.db.findNotifications({ + cardId: event.cardId, + messageId: event.messageId + }) + + if (notifications.length === 0) return [] + + const result: Event[] = [] + + const byContextId = groupByArray(notifications, (it) => it.contextId) + for (const [context, ns] of byContextId.entries()) { + result.push({ + type: NotificationEventType.RemoveNotifications, + contextId: context, + account: ns[0].account, + ids: notifications.map((it) => it.id) + }) + } + + return result +} + +const triggers: Triggers = [ + [ + 'on_notification_context_updated', + NotificationEventType.UpdateNotificationContext, + onNotificationContextUpdated as TriggerFn + ], + ['on_added_collaborators', NotificationEventType.AddCollaborators, onAddedCollaborators as TriggerFn], + ['on_removed_collaborators', NotificationEventType.RemoveCollaborators, onRemovedCollaborators as TriggerFn], + ['remove_notifications_on_messages_removed', MessageEventType.RemovePatch, onMessagesRemoved as TriggerFn] +] + +export default triggers diff --git a/foundations/communication/packages/server/src/triggers/utils.ts b/foundations/communication/packages/server/src/triggers/utils.ts new file mode 100644 index 0000000000..e8f6c76df4 --- /dev/null +++ b/foundations/communication/packages/server/src/triggers/utils.ts @@ -0,0 +1,55 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type AccountUuid, type Markdown, type SocialID } from '@hcengineering/communication-types' + +import { TriggerCtx } from '../types' + +export async function getNameBySocialID (ctx: TriggerCtx, id: SocialID): Promise { + const account = (await ctx.client.findPersonUuid(ctx, id, true)) as AccountUuid | undefined + return account != null ? ((await ctx.client.db.getNameByAccount(account)) ?? 'System') : 'System' +} + +export async function getAddCollaboratorsMessageContent ( + ctx: TriggerCtx, + sender: AccountUuid | undefined, + collaborators: AccountUuid[] +): Promise { + if (sender != null && collaborators.length === 1 && collaborators.includes(sender)) { + return 'Joined card' + } + + const collaboratorsNames = (await Promise.all(collaborators.map((it) => ctx.client.db.getNameByAccount(it)))).filter( + (it): it is string => it != null && it !== '' + ) + + return `Added ${collaboratorsNames.join(', ')}` +} + +export async function getRemoveCollaboratorsMessageContent ( + ctx: TriggerCtx, + sender: AccountUuid | undefined, + collaborators: AccountUuid[] +): Promise { + if (sender != null && collaborators.length === 1 && collaborators.includes(sender)) { + return 'Left card' + } + + const collaboratorsNames = (await Promise.all(collaborators.map((it) => ctx.client.db.getNameByAccount(it)))).filter( + (it): it is string => it != null && it !== '' + ) + + return `Removed ${collaboratorsNames.join(', ')}` +} diff --git a/foundations/communication/packages/server/src/types.ts b/foundations/communication/packages/server/src/types.ts new file mode 100644 index 0000000000..b01638392b --- /dev/null +++ b/foundations/communication/packages/server/src/types.ts @@ -0,0 +1,117 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Account, MeasureContext } from '@hcengineering/core' +import type { + EventResult, + Event, + SessionData, EventType +} from '@hcengineering/communication-sdk-types' +import type { + CardID, + Collaborator, + FindCollaboratorsParams, + FindLabelsParams, FindMessagesGroupParams, FindMessagesMetaParams, + FindNotificationContextParams, + FindNotificationsParams, + FindPeersParams, + Label, MessageMeta, MessagesGroup, + Notification, + NotificationContext, + Peer, + WorkspaceUuid +} from '@hcengineering/communication-types' + +import { LowLevelClient } from './client' + +export interface Metadata { + accountsUrl: string + hulylakeUrl: string + secret: string + messagesPerBlob: number +} + +export type Subscription = string | number + +export interface Middleware { + findMessagesMeta: (session: SessionData, params: FindMessagesMetaParams) => Promise + findMessagesGroups: (session: SessionData, params: FindMessagesGroupParams) => Promise + findNotificationContexts: ( + session: SessionData, + params: FindNotificationContextParams, + subscription?: Subscription + ) => Promise + findNotifications: ( + session: SessionData, + params: FindNotificationsParams, + subscription?: Subscription + ) => Promise + + findLabels: (session: SessionData, params: FindLabelsParams, subscription?: Subscription) => Promise + findCollaborators: (session: SessionData, params: FindCollaboratorsParams) => Promise + findPeers: (session: SessionData, params: FindPeersParams) => Promise + + event: (session: SessionData, event: Enriched, derived: boolean) => Promise + + subscribeCard: (session: SessionData, cardId: CardID, subscription: Subscription) => void + unsubscribeCard: (session: SessionData, cardId: CardID, subscription: Subscription) => void + + handleBroadcast: (session: SessionData, events: Enriched[]) => void + + closeSession: (sessionId: string) => void + close: () => void +} + +export interface MiddlewareContext { + ctx: MeasureContext + workspace: WorkspaceUuid + metadata: Metadata + client: LowLevelClient + + cadsWithPeers: Set + + derived?: Middleware + head?: Middleware +} + +export type MiddlewareCreateFn = (context: MiddlewareContext, next?: Middleware) => Promise + +export interface CommunicationCallbacks { + registerAsyncRequest: (ctx: MeasureContext, promise: (ctx: MeasureContext) => Promise) => void + broadcast: (ctx: MeasureContext, result: Record[]>) => void + enqueue: (ctx: MeasureContext, result: Enriched[]) => void +} + +export interface TriggerCtx { + ctx: MeasureContext + metadata: Metadata + client: LowLevelClient + workspace: WorkspaceUuid + account: Account + derived: boolean + processedPeersEvents: Set + execute: (event: Event) => Promise +} + +export type TriggerFn = (ctx: TriggerCtx, event: Enriched) => Promise +export type Triggers = [string, EventType, TriggerFn][] + +export type Enriched = T & { + _id: string + _eventExtra: Record + + skipPropagate?: boolean + date: Date +} diff --git a/foundations/communication/packages/server/tsconfig.json b/foundations/communication/packages/server/tsconfig.json new file mode 100644 index 0000000000..2ad347b11d --- /dev/null +++ b/foundations/communication/packages/server/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo", + "types": ["node", "jest"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} diff --git a/foundations/communication/packages/shared/.eslintrc.cjs b/foundations/communication/packages/shared/.eslintrc.cjs new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/communication/packages/shared/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/communication/packages/shared/CHANGELOG.json b/foundations/communication/packages/shared/CHANGELOG.json new file mode 100644 index 0000000000..153aedde84 --- /dev/null +++ b/foundations/communication/packages/shared/CHANGELOG.json @@ -0,0 +1,61 @@ +{ + "name": "@hcengineering/communication-shared", + "entries": [ + { + "version": "0.7.11", + "tag": "@hcengineering/communication-shared_v0.7.11", + "date": "Mon, 27 Oct 2025 16:28:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.11` to `0.7.12`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.11` to `0.7.12`" + } + ] + } + }, + { + "version": "0.7.8", + "tag": "@hcengineering/communication-shared_v0.7.8", + "date": "Sat, 25 Oct 2025 22:20:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.8` to `0.7.9`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.8` to `0.7.9`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/communication-shared_v0.7.4", + "date": "Tue, 14 Oct 2025 10:12:38 GMT", + "comments": { + "patch": [ + { + "comment": "Fix update patch event apply" + }, + { + "comment": "update hulylake client" + }, + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/communication/packages/shared/CHANGELOG.md b/foundations/communication/packages/shared/CHANGELOG.md new file mode 100644 index 0000000000..4c189468e2 --- /dev/null +++ b/foundations/communication/packages/shared/CHANGELOG.md @@ -0,0 +1,23 @@ +# Change Log - @hcengineering/communication-shared + +This log was last generated on Mon, 27 Oct 2025 16:28:25 GMT and should not be manually modified. + +## 0.7.11 +Mon, 27 Oct 2025 16:28:25 GMT + +_Version update only_ + +## 0.7.8 +Sat, 25 Oct 2025 22:20:35 GMT + +_Version update only_ + +## 0.7.4 +Tue, 14 Oct 2025 10:12:38 GMT + +### Patches + +- Fix update patch event apply +- update hulylake client +- update deps + diff --git a/foundations/communication/packages/shared/config/rig.json b/foundations/communication/packages/shared/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/communication/packages/shared/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/communication/packages/shared/package.json b/foundations/communication/packages/shared/package.json new file mode 100644 index 0000000000..f4f29c1fce --- /dev/null +++ b/foundations/communication/packages/shared/package.json @@ -0,0 +1,57 @@ +{ + "name": "@hcengineering/communication-shared", + "version": "0.7.11", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "tsconfig.json" + ], + "scripts": { + "build": "../scripts/compile.js", + "_phase:build": "compile transpile src", + "_phase:validate": "compile validate", + "lint": "eslint \"src/**/*.ts\"", + "lint:fix": "eslint --fix \"src/**/*.ts\"", + "format": "prettier --write src/**/*.ts && eslint --fix \"src/**/*.ts\" && echo 'Formatted'", + "clean": "rm -rf lib && rm -rf types rm -rf node_modules" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "esbuild": "^0.25.10", + "esbuild-plugin-copy": "^2.1.1", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "@types/jest": "^29.5.5", + "prettier": "^3.6.2", + "typescript": "^5.9.3" + }, + "dependencies": { + "@hcengineering/communication-types": "workspace:^0.7.12", + "@hcengineering/communication-sdk-types": "workspace:^0.7.12", + "@hcengineering/hulylake-client": "workspace:^0.7.17" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hcengineering/communication.git" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/communication/packages/shared/src/index.ts b/foundations/communication/packages/shared/src/index.ts new file mode 100644 index 0000000000..5805bbae84 --- /dev/null +++ b/foundations/communication/packages/shared/src/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './retry' +export * from './utils' +export * from './processor' diff --git a/foundations/communication/packages/shared/src/processor.ts b/foundations/communication/packages/shared/src/processor.ts new file mode 100644 index 0000000000..d32a28e2ae --- /dev/null +++ b/foundations/communication/packages/shared/src/processor.ts @@ -0,0 +1,437 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + AttachmentID, + ContextID, + Message, + MessageID, + Notification, + NotificationContext, + NotificationID, + PersonUuid, + Emoji, + AttachmentData, + SocialID, + Attachment, + AttachmentUpdateData, + CardID, + CardType +} from '@hcengineering/communication-types' +import { + AddAttachmentsOperation, + CreateMessageEvent, + CreateNotificationContextEvent, + CreateNotificationEvent, + MessageEventType, + PatchEvent, + RemoveAttachmentsOperation, + RemoveNotificationContextEvent, + SetAttachmentsOperation, + UpdateAttachmentsOperation, + UpdateNotificationContextEvent, + ReactionPatchEvent, + AttachmentPatchEvent, + BlobPatchEvent, + ThreadPatchEvent +} from '@hcengineering/communication-sdk-types' + +import { withTotal } from './utils' + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class MessageProcessor { + static create (event: CreateMessageEvent, id?: MessageID): Message { + const messageId = event.messageId ?? (id as MessageID) + if (messageId == null) throw new Error('Message id is required') + return { + id: messageId, + cardId: event.cardId, + type: event.messageType, + content: event.content, + extra: event.extra, + creator: event.socialId, + created: new Date(event.date ?? Date.now()), + language: event.language, + reactions: {}, + attachments: [], + threads: [] + } + } + + static applyPatch (message: Message, patchEvent: PatchEvent): Message | undefined { + return applyPatchEvent(message, patchEvent) + } +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class NotificationContextProcessor { + static create (event: CreateNotificationContextEvent, id?: ContextID): NotificationContext { + const contextId: ContextID | undefined = event.contextId ?? id + + if (contextId == null) { + throw new Error('Notification context id is required') + } + return { + id: contextId, + cardId: event.cardId, + account: event.account, + lastView: event.lastView, + lastUpdate: event.lastUpdate, + lastNotify: event.lastNotify, + notifications: withTotal([] as Notification[]) + } + } + + static update (context: NotificationContext, event: UpdateNotificationContextEvent): NotificationContext { + if (context.account !== event.account || context.id !== event.contextId) return context + return { + ...context, + lastView: event.updates.lastView ?? context.lastView, + lastUpdate: event.updates.lastUpdate ?? context.lastUpdate, + lastNotify: event.updates.lastNotify ?? context.lastNotify + } + } + + static remove (context: NotificationContext, event: RemoveNotificationContextEvent): NotificationContext | undefined { + if (context.account !== event.account || context.id !== event.contextId) return context + return undefined + } +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class NotificationProcessor { + static create (event: CreateNotificationEvent, id?: NotificationID): Notification { + const notificationId: NotificationID | undefined = event.notificationId ?? (id as NotificationID) + + if (notificationId == null) { + throw new Error('Notification id is required') + } + return { + id: notificationId, + cardId: event.cardId, + contextId: event.contextId, + account: event.account, + type: event.notificationType, + read: event.read, + content: event.content ?? {}, + created: event.date ?? new Date(), + messageId: event.messageId, + creator: event.creator, + blobId: event.blobId + } + } +} + +function applyPatchEvent (message: Message, event: PatchEvent): Message | undefined { + if (message.cardId !== event.cardId || message.id !== event.messageId) return message + const date = event.date ?? new Date() + + switch (event.type) { + case MessageEventType.UpdatePatch: { + if (date.getTime() < (message.modified?.getTime() ?? 0)) { + return message + } + const shouldUpdateDate = event.content != null || event.extra != null + return { + ...message, + modified: shouldUpdateDate ? date : message.modified, + content: event.content ?? message.content, + extra: event.extra ?? message.extra, + language: event.language ?? message.language + } + } + case MessageEventType.RemovePatch: { + return undefined + } + case MessageEventType.ReactionPatch: + return patchReactions(message, event, date) + case MessageEventType.AttachmentPatch: + return patchAttachments(message, event, date) + case MessageEventType.BlobPatch: + return patchBlobs(message, event, date) + case MessageEventType.ThreadPatch: + return patchThread(message, event, date) + } + return message +} + +function patchReactions (message: Message, event: ReactionPatchEvent, date: Date): Message { + if (event.personUuid == null) return message + if (event.operation.opcode === 'add') { + return addReaction(message, event.operation.reaction, event.personUuid, date) + } else if (event.operation.opcode === 'remove') { + return removeReaction(message, event.operation.reaction, event.personUuid) + } + return message +} + +function addReaction (message: Message, emoji: Emoji, creator: PersonUuid, created: Date): Message { + const emojiData = message.reactions[emoji] ?? [] + if (emojiData.some((it) => it.person === creator)) return message + + const data = { + count: 1, + person: creator, + date: created + } + + message.reactions = { + ...message.reactions, + [emoji]: [...emojiData, data] + } + return message +} + +function removeReaction (message: Message, emoji: Emoji, creator: PersonUuid): Message { + const emojiData = message.reactions[emoji] ?? [] + if (!emojiData.some((it) => it.person === creator)) return message + + return { + ...message, + reactions: { + ...message.reactions, + [emoji]: emojiData.filter((it) => it.person !== creator) + } + } +} + +function patchAttachments (message: Message, event: AttachmentPatchEvent, date: Date): Message { + for (const op of event.operations) { + if (op.opcode === 'add') { + return addAttachments(message, op.attachments, event.socialId, date) + } else if (op.opcode === 'remove') { + return removeAttachments(message, op.ids) + } else if (op.opcode === 'set') { + return setAttachments(message, op.attachments, event.socialId, date) + } else if (op.opcode === 'update') { + return updateAttachments(message, op.attachments, date) + } + } + return message +} + +function patchBlobs (message: Message, event: BlobPatchEvent, date: Date): Message { + const operations: ( + | AddAttachmentsOperation + | RemoveAttachmentsOperation + | SetAttachmentsOperation + | UpdateAttachmentsOperation + )[] = [] + for (const op of event.operations) { + if (op.opcode === 'attach') { + operations.push({ + opcode: 'add', + attachments: op.blobs.map((it) => ({ + id: it.blobId as any as AttachmentID, + mimeType: it.mimeType ?? it.mimeType, + params: it + })) + }) + } else if (op.opcode === 'detach') { + operations.push({ + opcode: 'remove', + ids: op.blobIds as any as AttachmentID[] + }) + } else if (op.opcode === 'set') { + operations.push({ + opcode: 'set', + attachments: op.blobs.map((it) => ({ + id: it.blobId as any as AttachmentID, + mimeType: it.mimeType ?? it.mimeType, + params: it + })) + }) + } else if (op.opcode === 'update') { + operations.push({ + opcode: 'update', + attachments: op.blobs.map((it) => ({ + id: it.blobId as any as AttachmentID, + params: it + })) + }) + } + } + + const ev: AttachmentPatchEvent = { + type: MessageEventType.AttachmentPatch, + cardId: message.cardId, + messageId: message.id, + operations, + socialId: event.socialId, + date: event.date + } + + return patchAttachments(message, ev, date) +} + +function addAttachments (message: Message, data: AttachmentData[], creator: SocialID, created: Date): Message { + const newAttachments: Attachment[] = [] + for (const attach of data) { + const isExists = message.attachments.some((it) => it.id === attach.id) + if (isExists === undefined) continue + const attachment: Attachment = { + ...attach, + created, + creator + } as any + newAttachments.push(attachment) + } + + if (newAttachments.length === 0) return message + return { + ...message, + attachments: [...message.attachments, ...newAttachments] + } +} + +function updateAttachments (message: Message, updates: AttachmentUpdateData[], date: Date): Message { + if (updates.length === 0) return message + const updatedAttachments: Attachment[] = [] + for (const attachment of message.attachments) { + const update = updates.find((it) => it.id === attachment.id) + if (update === undefined) { + updatedAttachments.push(attachment) + } else { + updatedAttachments.push({ + ...attachment, + params: { + ...attachment.params, + ...update.params + }, + modified: date.getTime() > (attachment.modified?.getTime() ?? 0) ? date : attachment.modified + } as any) + } + } + + return { + ...message, + attachments: updatedAttachments + } +} + +function removeAttachments (message: Message, ids: AttachmentID[]): Message { + const attachments = message.attachments.filter((it) => !ids.includes(it.id)) + if (attachments.length === message.attachments.length) return message + + return { + ...message, + attachments + } +} + +function setAttachments (message: Message, data: AttachmentData[], creator: SocialID, created: Date): Message { + if (data.length === 0) return message + return { + ...message, + attachments: data.map( + (it) => + ({ + ...it, + created, + creator + }) as any + ) + } +} + +function patchThread (message: Message, event: ThreadPatchEvent, date: Date): Message { + const { operation } = event + if (operation.opcode === 'attach') { + return attachThread(message, operation.threadId, operation.threadType) + } else if (operation.opcode === 'update') { + return updateThread(message, operation.threadId, operation.update.threadType) + } else if (operation.opcode === 'addReply') { + if (event.personUuid == null) return message + return addReply(message, operation.threadId, event.personUuid, date) + } else if (operation.opcode === 'removeReply') { + if (event.personUuid == null) return message + return removeReply(message, operation.threadId, event.personUuid) + } + return message +} + +function attachThread (message: Message, threadId: CardID, threadType: CardType): Message { + if (message.threads.some((thread) => thread.threadId === threadId)) return message + return { + ...message, + threads: [ + ...message.threads, + { + cardId: message.cardId, + messageId: message.id, + threadId, + threadType, + repliesCount: 0, + lastReplyDate: undefined, + repliedPersons: {} + } + ] + } +} + +function updateThread (message: Message, threadId: CardID, threadType: CardType): Message { + if (message.threads.length === 0) return message + const t = message.threads.find((it) => it.threadId === threadId) + if (t === undefined) return message + + return { + ...message, + threads: message.threads.map((it) => + it.threadId === threadId + ? { + ...it, + threadType + } + : it + ) + } +} + +function addReply (message: Message, threadId: CardID, person: PersonUuid, date: Date): Message { + if (!message.threads.some((thread) => thread.threadId === threadId)) return message + return { + ...message, + threads: message.threads.map((it) => + it.threadId === threadId + ? { + ...it, + repliesCount: it.repliesCount + 1, + lastReplyDate: date.getTime() > (it.lastReplyDate?.getTime() ?? 0) ? date : it.lastReplyDate, + repliedPersons: { + ...it.repliedPersons, + [person]: (it.repliedPersons[person] ?? 0) + 1 + } + } + : it + ) + } +} + +function removeReply (message: Message, threadId: CardID, person: PersonUuid): Message { + if (!message.threads.some((thread) => thread.threadId === threadId)) return message + return { + ...message, + threads: message.threads.map((it) => + it.threadId === threadId + ? { + ...it, + repliesCount: Math.max(0, it.repliesCount - 1), + repliedPersons: { + ...it.repliedPersons, + [person]: Math.max(0, (it.repliedPersons[person] ?? 0) - 1) + } + } + : it + ) + } +} diff --git a/foundations/communication/packages/shared/src/retry.ts b/foundations/communication/packages/shared/src/retry.ts new file mode 100644 index 0000000000..455f4a5709 --- /dev/null +++ b/foundations/communication/packages/shared/src/retry.ts @@ -0,0 +1,35 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +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/foundations/communication/packages/shared/src/utils.ts b/foundations/communication/packages/shared/src/utils.ts new file mode 100644 index 0000000000..b677e6aa10 --- /dev/null +++ b/foundations/communication/packages/shared/src/utils.ts @@ -0,0 +1,251 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + AppletAttachment, + Attachment, + BlobAttachment, + BlobID, + CardID, + type Emoji, + FindMessagesOptions, + FindMessagesParams, + LinkPreviewAttachment, + linkPreviewType, + Message, + MessageDoc, + MessageID, + MessagesDoc, + MessagesGroup, + MessagesGroupDoc, + MessagesGroupsDoc, + type PersonUuid, + SortingOrder, + TranslatedMessage, + TranslatedMessagesDoc, + WithTotal +} from '@hcengineering/communication-types' +import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client' + +const COUNTER_BITS = 10n +const RANDOM_BITS = 10n +const MAX_SEQUENCE = (1n << COUNTER_BITS) - 1n + +let counter = 0n + +function makeBigIntId (): bigint { + const ts = BigInt(Date.now()) + counter = counter < MAX_SEQUENCE ? counter + 1n : 0n + const random = BigInt(Math.floor(Math.random() * Number((1n << RANDOM_BITS) - 1n))) + return (ts << (COUNTER_BITS + RANDOM_BITS)) | (counter << RANDOM_BITS) | random +} + +function toBase64Url (bytes: Uint8Array): string { + let s = '' + for (const b of bytes) s += String.fromCharCode(b) + // @ts-expect-error browser btoa / node Buffer + const base64 = typeof btoa === 'function' ? btoa(s) : Buffer.from(bytes).toString('base64') + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +} + +export function generateMessageId (): MessageID { + const idBig = makeBigIntId() + const buf = new Uint8Array(8) + new DataView(buf.buffer).setBigUint64(0, idBig, false) + return toBase64Url(buf) as MessageID +} + +export function isAppletAttachment (attachment: Attachment): attachment is AppletAttachment { + return attachment.mimeType.startsWith('application/vnd.huly.applet.') +} + +export function isLinkPreviewAttachmentType (mimeType: string): boolean { + return mimeType === linkPreviewType +} + +export function isAppletAttachmentType (mimeType: string): boolean { + return mimeType.startsWith('application/vnd.huly.applet.') +} + +export function isBlobAttachmentType (mimeType: string): boolean { + return !isLinkPreviewAttachmentType(mimeType) && !isAppletAttachmentType(mimeType) +} + +export function isLinkPreviewAttachment (attachment: Attachment): attachment is LinkPreviewAttachment { + return attachment.mimeType === linkPreviewType +} + +export function isBlobAttachment (attachment: Attachment): attachment is BlobAttachment { + return !isLinkPreviewAttachment(attachment) && !isAppletAttachment(attachment) && 'blobId' in attachment.params +} + +export function withTotal (objects: T[], total?: number): WithTotal { + const length = total ?? objects.length + + return Object.assign(objects, { total: length }) +} + +export async function loadMessagesGroups (client: HulylakeWorkspaceClient, cardId: CardID): Promise { + const res = await client.getJson(`${cardId}/messages/groups`, { + maxRetries: 3, + isRetryable: () => true, + delayStrategy: { + getDelay: () => 500 + } + }) + + if (res?.body === undefined) { + return [] + } + return Object.values(res.body) + .map((it) => deserializeMessageGroup(it)) + .sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime()) +} + +function deserializeMessageGroup (group: MessagesGroupDoc): MessagesGroup { + return { + cardId: group.cardId, + blobId: group.blobId, + fromDate: new Date(group.fromDate), + toDate: new Date(group.toDate), + count: group.count + } +} + +export async function loadMessages ( + client: HulylakeWorkspaceClient, + blobId: BlobID, + params: FindMessagesParams, + options?: FindMessagesOptions +): Promise { + const { cardId } = params + const res = await client.getJson(`${cardId}/messages/${blobId}`, { + maxRetries: 3, + isRetryable: () => true, + delayStrategy: { + getDelay: () => 500 + } + }) + if (res?.body === undefined) { + return [] + } + return parseMessagesDoc(res.body, params, options) +} + +export function parseMessagesDoc ( + json: MessagesDoc, + params: FindMessagesParams, + options?: FindMessagesOptions +): Message[] { + let messages: Record = {} + if (params.id != null) { + const value = (json.messages as any)[params.id] + + if (value == null) { + return [] + } + + messages = { + [params.id]: value + } + } else { + messages = json.messages + } + + const result: Message[] = [] + for (const m of Object.values(messages)) { + if (params.limit != null && result.length >= params.limit) break + const message: Message = { + id: m.id, + cardId: m.cardId, + created: new Date(m.created), + creator: m.creator, + type: m.type, + content: m.content, + extra: m.extra, + language: m.language ?? undefined, + modified: m.modified != null ? new Date(m.modified) : undefined, + reactions: {}, + attachments: [], + threads: [] + } + + if (options?.reactions === true) { + for (const [emoji, users] of Object.entries(m.reactions)) { + for (const [user, data] of Object.entries(users)) { + const messageData = message.reactions[emoji as Emoji] ?? [] + messageData.push({ + count: Number(data.count), + person: user as PersonUuid, + date: new Date(data.date) + }) + message.reactions[emoji as Emoji] = messageData + } + } + } + + if (options?.attachments === true) { + for (const attachment of Object.values(m.attachments)) { + message.attachments.push({ + id: attachment.id, + mimeType: attachment.mimeType, + params: attachment.params as any, + creator: m.creator, + created: new Date(m.created) + }) + } + } + + if (options?.threads === true) { + for (const thread of Object.values(m.threads)) { + message.threads.push({ + cardId: m.cardId, + messageId: m.id, + threadId: thread.threadId, + threadType: thread.threadType, + repliesCount: Number(thread.repliesCount), + lastReplyDate: thread.lastReplyDate != null ? new Date(thread.lastReplyDate) : undefined, + repliedPersons: thread.repliedPersons + }) + } + } + + result.push(message) + } + + if (params.order === SortingOrder.Ascending) { + result.sort((a, b) => a.created.getTime() - b.created.getTime()) + } else if (params.order === SortingOrder.Descending) { + result.sort((a, b) => b.created.getTime() - a.created.getTime()) + } + return result +} + +export function parseTranslatedMessagesDoc (json: TranslatedMessagesDoc): TranslatedMessage[] { + const result: TranslatedMessage[] = [] + for (const m of Object.values(json.messages)) { + const message: TranslatedMessage = { + id: m.id, + created: new Date(m.created), + creator: m.creator, + content: m.content + } + + result.push(message) + } + + result.sort((a, b) => a.created.getTime() - b.created.getTime()) + return result +} diff --git a/foundations/communication/packages/shared/tsconfig.json b/foundations/communication/packages/shared/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/communication/packages/shared/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/communication/packages/types/.eslintrc.cjs b/foundations/communication/packages/types/.eslintrc.cjs new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/communication/packages/types/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/communication/packages/types/CHANGELOG.json b/foundations/communication/packages/types/CHANGELOG.json new file mode 100644 index 0000000000..7f11edfb9e --- /dev/null +++ b/foundations/communication/packages/types/CHANGELOG.json @@ -0,0 +1,44 @@ +{ + "name": "@hcengineering/communication-types", + "entries": [ + { + "version": "0.7.12", + "tag": "@hcengineering/communication-types_v0.7.12", + "date": "Mon, 27 Oct 2025 16:28:25 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ] + } + }, + { + "version": "0.7.9", + "tag": "@hcengineering/communication-types_v0.7.9", + "date": "Sat, 25 Oct 2025 22:20:35 GMT", + "comments": { + "patch": [ + { + "comment": "Add collaborativeChange activity update" + }, + { + "comment": "Bump core" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/communication-types_v0.7.4", + "date": "Tue, 14 Oct 2025 10:12:38 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ] + } + } + ] +} diff --git a/foundations/communication/packages/types/CHANGELOG.md b/foundations/communication/packages/types/CHANGELOG.md new file mode 100644 index 0000000000..1f1d95096b --- /dev/null +++ b/foundations/communication/packages/types/CHANGELOG.md @@ -0,0 +1,26 @@ +# Change Log - @hcengineering/communication-types + +This log was last generated on Mon, 27 Oct 2025 16:28:25 GMT and should not be manually modified. + +## 0.7.12 +Mon, 27 Oct 2025 16:28:25 GMT + +### Patches + +- update deps + +## 0.7.9 +Sat, 25 Oct 2025 22:20:35 GMT + +### Patches + +- Add collaborativeChange activity update +- Bump core + +## 0.7.4 +Tue, 14 Oct 2025 10:12:38 GMT + +### Patches + +- update deps + diff --git a/foundations/communication/packages/types/config/rig.json b/foundations/communication/packages/types/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/communication/packages/types/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/communication/packages/types/package.json b/foundations/communication/packages/types/package.json new file mode 100644 index 0000000000..c8e4fa5878 --- /dev/null +++ b/foundations/communication/packages/types/package.json @@ -0,0 +1,55 @@ +{ + "name": "@hcengineering/communication-types", + "version": "0.7.12", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "tsconfig.json" + ], + "scripts": { + "build": "compile", + "_phase:build": "compile transpile src", + "_phase:validate": "compile validate", + "lint": "eslint \"src/**/*.ts\"", + "lint:fix": "eslint --fix \"src/**/*.ts\"", + "format": "prettier --write src/**/*.ts && eslint --fix \"src/**/*.ts\" && echo 'Formatted'", + "clean": "rm -rf lib && rm -rf types rm -rf node_modules" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "esbuild": "^0.25.10", + "esbuild-plugin-copy": "^2.1.1", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "@types/jest": "^29.5.5", + "prettier": "^3.6.2", + "typescript": "^5.9.3" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hcengineering/communication.git" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/communication/packages/types/src/core.ts b/foundations/communication/packages/types/src/core.ts new file mode 100644 index 0000000000..0f096e10ec --- /dev/null +++ b/foundations/communication/packages/types/src/core.ts @@ -0,0 +1,26 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Ref, Blob, AccountUuid, WorkspaceUuid, PersonId, BlobMetadata, PersonUuid } from '@hcengineering/core' + +export type { AccountUuid, PersonUuid, WorkspaceUuid, BlobMetadata } + +export type BlobID = Ref +export type CardID = Ref +export type CardType = Ref +export type SocialID = PersonId + +export type Markdown = string +export type ID = string diff --git a/foundations/communication/packages/types/src/file.ts b/foundations/communication/packages/types/src/file.ts new file mode 100644 index 0000000000..1acbf257bd --- /dev/null +++ b/foundations/communication/packages/types/src/file.ts @@ -0,0 +1,81 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { BlobID, CardID, CardType, Markdown, PersonUuid, SocialID } from './core' +import { MessageID, MessageType, MessageExtra, AttachmentID, AttachmentParams, Emoji } from './message' + +export interface MessagesDoc { + cardId: CardID + fromDate: string // ISO date + toDate: string // ISO date + messages: Record + language: string +} + +export interface MessageDoc { + id: MessageID + cardId: CardID + created: string // ISO date + creator: SocialID + type: MessageType + content: Markdown + extra: MessageExtra + language: string | null + modified: string | null // ISO date + + reactions: Record> + attachments: Record + threads: Record +} + +export interface AttachmentDoc { + id: AttachmentID + mimeType: string + params: AttachmentParams + creator: SocialID + created: string // ISO date + modified: string | null // ISO date +} + +export interface ThreadDoc { + threadId: CardID + threadType: CardType + repliesCount: number + lastReplyDate: string | null // ISO date + repliedPersons: Record +} + +export type MessagesGroupsDoc = Record + +export interface MessagesGroupDoc { + cardId: CardID + blobId: BlobID + fromDate: string // ISO date + toDate: string // ISO date + count: number +} + +export interface TranslatedMessagesDoc { + cardId: CardID + messages: Record + language: string +} + +export interface TranslatesMessageDoc { + id: MessageID + created: string // ISO date + creator: SocialID + content: Markdown +} diff --git a/foundations/communication/packages/types/src/index.ts b/foundations/communication/packages/types/src/index.ts new file mode 100644 index 0000000000..7411108b88 --- /dev/null +++ b/foundations/communication/packages/types/src/index.ts @@ -0,0 +1,22 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './core' +export * from './file' +export * from './message' +export * from './notification' +export * from './query' +export * from './label' +export * from './peer' diff --git a/foundations/communication/packages/types/src/label.ts b/foundations/communication/packages/types/src/label.ts new file mode 100644 index 0000000000..19f96dfe5e --- /dev/null +++ b/foundations/communication/packages/types/src/label.ts @@ -0,0 +1,28 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { AccountUuid, CardID, CardType } from './core' + +export type LabelID = string & { __label: true } + +export const SubscriptionLabelID = 'card:label:Subscribed' as LabelID + +export interface Label { + labelId: LabelID + cardId: CardID + cardType: CardType + account: AccountUuid + created: Date +} diff --git a/foundations/communication/packages/types/src/message.ts b/foundations/communication/packages/types/src/message.ts new file mode 100644 index 0000000000..4b94d926eb --- /dev/null +++ b/foundations/communication/packages/types/src/message.ts @@ -0,0 +1,215 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Attribute, BlobMetadata, Class, Mixin, Ref } from '@hcengineering/core' + +import type { AccountUuid, BlobID, CardID, CardType, ID, Markdown, SocialID, PersonUuid } from './core' + +// Message +export type MessageID = ID & { message: true } +export type Emoji = string & { emoji: true } + +export enum MessageType { + Text = 'text', + Activity = 'activity' +} + +export type MessageExtra = Record + +export interface Message { + id: MessageID + cardId: CardID + + type: MessageType + content: Markdown + extra?: MessageExtra + language?: string + + creator: SocialID + created: Date + modified?: Date + + reactions: Record + attachments: Attachment[] + threads: Thread[] + translates?: Record +} + +export type TranslatedMessage = Pick + +export interface EmojiData { + count: number + person: PersonUuid + date: Date +} + +export type MessageMeta = Pick & { blobId: BlobID } + +export interface ActivityMessage extends Message { + type: MessageType.Activity + extra: ActivityMessageExtra +} + +export interface ActivityMessageExtra { + action: 'create' | 'remove' | 'update' + update?: ActivityUpdate +} + +export type ActivityUpdate = + | ActivityAttributeUpdate + | ActivityTagUpdate + | ActivityTypeUpdate + | ActivityCollaboratorsUpdate + | ActivityProcess + | ActivityCollaborativeChange + +export enum ActivityUpdateType { + Attribute = 'attribute', + Tag = 'tag', + Collaborators = 'collaborators', + Type = 'type', + Process = 'process', + CollaborativeChange = 'collaborativeChange' +} + +export interface ActivityProcess { + type: ActivityUpdateType.Process + process: Ref + action: 'started' | 'complete' | 'transition' + transitionTo?: Ref +} + +export interface ActivityTagUpdate { + type: ActivityUpdateType.Tag + tag: Ref + action: 'add' | 'remove' +} + +export interface ActivityCollaboratorsUpdate { + type: ActivityUpdateType.Collaborators + added: AccountUuid[] + removed: AccountUuid[] +} + +export interface ActivityTypeUpdate { + type: ActivityUpdateType.Type + newType: CardType +} + +export interface ActivityCollaborativeChange { + type: ActivityUpdateType.CollaborativeChange + attrKey: string + value: string + prevValue: string +} + +type AttributeValue = string | number | null + +export interface ActivityAttributeUpdate { + type: ActivityUpdateType.Attribute + attrKey: string + attrClass: Ref>> + mixin?: Ref> + set?: AttributeValue | AttributeValue[] + added?: AttributeValue[] + removed?: AttributeValue[] +} + +// LinkPreview +export const linkPreviewType = 'application/vnd.huly.link-preview' as const + +export interface LinkPreviewParams { + url: string + host: string + + title?: string + description?: string + siteName?: string + + iconUrl?: string + previewImage?: LinkPreviewImage +} + +export interface LinkPreviewImage { + url: string + width?: number + height?: number +} + +export interface BlobParams { + blobId: BlobID + fileName: string + size: number + metadata?: BlobMetadata +} + +// Attachment +export type AttachmentID = string & { __attachmentId: true } + +export type Attachment = BlobAttachment | LinkPreviewAttachment | AppletAttachment + +interface BaseAttachment extends AttachmentData { + creator: SocialID + created: Date + modified?: Date +} + +export interface LinkPreviewAttachment extends BaseAttachment { + mimeType: typeof linkPreviewType +} + +export interface BlobAttachment extends BaseAttachment {} + +export type AppletParams = Record +export type AppletType = `application/vnd.huly.applet.${string}` + +export interface AppletAttachment extends BaseAttachment { + mimeType: AppletType +} + +export interface AttachmentData

{ + id: AttachmentID + mimeType: string + params: P +} + +export type AttachmentParams = Record + +export interface AttachmentUpdateData

{ + id: AttachmentID + params: Partial

+} + +// Thread +export interface Thread { + cardId: CardID + messageId: MessageID + threadId: CardID + threadType: CardType + repliesCount: number + lastReplyDate: Date | undefined + repliedPersons: Record +} + +export type ThreadMeta = Pick + +// MessagesGroup +export interface MessagesGroup { + cardId: CardID + blobId: BlobID + fromDate: Date + toDate: Date + count: number +} diff --git a/foundations/communication/packages/types/src/notification.ts b/foundations/communication/packages/types/src/notification.ts new file mode 100644 index 0000000000..6347c5df4f --- /dev/null +++ b/foundations/communication/packages/types/src/notification.ts @@ -0,0 +1,69 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { AccountUuid, BlobID, CardID, CardType, ID, SocialID } from './core' +import type { Message, MessageID } from './message' + +export type ContextID = ID & { context: true } +export type NotificationID = ID & { notification: true } + +export interface Collaborator { + cardId: CardID + cardType: CardType + account: AccountUuid +} + +export interface Notification { + id: NotificationID + cardId: CardID + contextId: ContextID + account: AccountUuid + type: NotificationType + read: boolean + created: Date + content: NotificationContent + messageId: MessageID + creator: SocialID + blobId: BlobID + + message?: Message +} + +export enum NotificationType { + Message = 'message', + Reaction = 'reaction' +} + +export type NotificationContent = { + title: string + shortText: string + senderName: string +} & Record + +export type ReactionNotificationContent = NotificationContent & { + emoji: string +} + +export interface NotificationContext { + id: ContextID + cardId: CardID + account: AccountUuid + lastUpdate: Date + lastView: Date + lastNotify?: Date + + notifications?: Notification[] + totalNotifications?: number +} diff --git a/foundations/communication/packages/types/src/peer.ts b/foundations/communication/packages/types/src/peer.ts new file mode 100644 index 0000000000..03739ff51e --- /dev/null +++ b/foundations/communication/packages/types/src/peer.ts @@ -0,0 +1,43 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CardID, WorkspaceUuid } from './core' + +export type PeerKind = 'card' | string +export type PeerExtra = Record + +interface BasePeer { + workspaceId: WorkspaceUuid + cardId: CardID + kind: PeerKind + value: string + extra: PeerExtra + created: Date +} + +export interface CardPeer extends BasePeer { + kind: 'card' + members: CardPeerMember[] +} + +export interface ExternalPeer extends BasePeer { + kind: string +} + +export type Peer = CardPeer | ExternalPeer + +export interface CardPeerMember { + workspaceId: WorkspaceUuid + cardId: CardID + extra: PeerExtra +} diff --git a/foundations/communication/packages/types/src/query.ts b/foundations/communication/packages/types/src/query.ts new file mode 100644 index 0000000000..51a4fd1982 --- /dev/null +++ b/foundations/communication/packages/types/src/query.ts @@ -0,0 +1,122 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { SortingOrder } from '@hcengineering/core' + +import type { MessageID } from './message' +import type { ContextID, NotificationID, NotificationType } from './notification' +import type { AccountUuid, BlobID, CardID, CardType, SocialID, WorkspaceUuid } from './core' +import type { LabelID } from './label' +import { PeerKind } from './peer' + +export { SortingOrder } + +export type ComparisonOperator = 'less' | 'lessOrEqual' | 'greater' | 'greaterOrEqual' | 'notEqual' + +export interface Window { + getResult: () => T[] + getTotal: () => number + + loadNextPage: () => Promise + + loadPrevPage: () => Promise + + hasNextPage: () => boolean + + hasPrevPage: () => boolean +} + +interface FindParams { + order?: SortingOrder + limit?: number +} + +export interface FindMessagesParams extends FindParams { + cardId: CardID + id?: MessageID +} + +export interface FindMessagesMetaParams extends FindParams { + cardId?: CardID + id?: MessageID + creator?: SocialID +} + +export interface FindMessagesGroupParams extends FindParams { + cardId: CardID + id?: MessageID + blobId?: BlobID + fromDate?: Partial> | Date + toDate?: Partial> | Date +} + +export interface FindMessagesOptions { + attachments?: boolean + reactions?: boolean + threads?: boolean +} + +export interface FindNotificationContextParams extends FindParams { + id?: ContextID + cardId?: CardID | CardID[] + lastNotify?: Partial> | Date + account?: AccountUuid | AccountUuid[] + notifications?: { + type?: NotificationType + limit: number + order: SortingOrder + read?: boolean + total?: boolean + } +} + +export interface FindNotificationsParams extends FindParams { + id?: NotificationID + messageId?: MessageID + type?: NotificationType + contextId?: ContextID + read?: boolean + created?: Partial> | Date + account?: AccountUuid | AccountUuid[] + cardId?: CardID + total?: boolean +} + +export interface FindCollaboratorsParams extends FindParams { + cardId: CardID + account?: AccountUuid | AccountUuid[] +} + +export interface FindLabelsParams extends FindParams { + labelId?: LabelID | LabelID[] + cardId?: CardID + cardType?: CardType | CardType[] + account?: AccountUuid +} + +export interface FindThreadMetaParams extends FindParams { + cardId?: CardID + messageId?: MessageID + threadId?: CardID +} + +export interface FindPeersParams extends FindParams { + workspaceId?: WorkspaceUuid + cardId?: CardID + kind?: PeerKind + value?: string +} + +export type WithTotal = T[] & { total: number } diff --git a/foundations/communication/packages/types/tsconfig.json b/foundations/communication/packages/types/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/communication/packages/types/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/communication/rush.json b/foundations/communication/rush.json new file mode 100644 index 0000000000..178197ed8d --- /dev/null +++ b/foundations/communication/rush.json @@ -0,0 +1,73 @@ +/** + * This is the main configuration file for Rush. + * For full documentation, please see https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", + "rushVersion": "5.153.2", + "pnpmVersion": "9.15.3", + "nodeSupportedVersionRange": ">=20.0.0 <25.0.0", + "projectFolderMaxDepth": 2, + "gitPolicy": {}, + "repository": { + "url": "https://github.com/hcengineering/communication", + "defaultBranch": "main", + "defaultRemote": "origin" + }, + "eventHooks": { + "preRushInstall": [], + "postRushInstall": [], + "preRushBuild": [], + "postRushBuild": [], + "preRushx": [], + "postRushx": [] + }, + "variants": [], + "projects": [ + { + "packageName": "@hcengineering/scripts", + "projectFolder": "common/scripts", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/communication-types", + "projectFolder": "packages/types", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/communication-sdk-types", + "projectFolder": "packages/sdk-types", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/communication-shared", + "projectFolder": "packages/shared", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/communication-rest-client", + "projectFolder": "packages/rest-client", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/communication-cockroach", + "projectFolder": "packages/cockroach", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/communication-server", + "projectFolder": "packages/server", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/communication-query", + "projectFolder": "packages/query", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/communication-client-query", + "projectFolder": "packages/client-query", + "shouldPublish": true + } + ] +} diff --git a/foundations/communication/translate.json b/foundations/communication/translate.json new file mode 100644 index 0000000000..5b97bfd669 --- /dev/null +++ b/foundations/communication/translate.json @@ -0,0 +1,15 @@ +{ + "cardId": "card-000", + "fromDate": "2025-09-02T10:00:00.000Z", + "toDate": "2025-09-02T12:30:00.000Z", + "language": "en", + + "messages": { + "msg-001": { + "id": "msg-001", + "cardId": "card-000", + "type": "text", + "content": "Привет! Это первое сообщение." + } + } +} diff --git a/foundations/core/.gitattributes b/foundations/core/.gitattributes new file mode 100644 index 0000000000..79a85db5c2 --- /dev/null +++ b/foundations/core/.gitattributes @@ -0,0 +1,14 @@ +# Don't allow people to merge changes to these generated files, because the result +# may be invalid. You need to run "rush update" again. +pnpm-lock.yaml merge=text +shrinkwrap.yaml merge=binary +npm-shrinkwrap.json merge=binary +yarn.lock merge=binary + +# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic +# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor +# may also require a special configuration to allow comments in JSON. +# +# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 +# +*.json linguist-language=JSON-with-Comments diff --git a/foundations/core/.github/copilot-instructions.md b/foundations/core/.github/copilot-instructions.md new file mode 100644 index 0000000000..bc7e1960df --- /dev/null +++ b/foundations/core/.github/copilot-instructions.md @@ -0,0 +1,69 @@ +# GitHub Copilot Instructions - Huly Core + +**Type**: Rush monorepo (TypeScript) +**License**: EPL-2.0 +**Build System**: Rush v5.158.1 + pnpm v10.15.1 + +## Critical Rules + +1. **Always use Rush commands**, never npm/pnpm directly: `rush install`, `rush build`, `rush test`, `rush update` +2. **Internal dependencies must use `workspace:^` protocol** in package.json +3. **All packages extend `@hcengineering/platform-rig`** for tsconfig/eslint - don't override configs +4. **Co-locate tests** in `src/__tests__/` directories (Jest + ts-jest) +5. **Named exports only** - avoid default exports + +## Quick Start + +```bash +rush install && rush build # Initial setup +rush build:watch # Development mode +rush rebuild # Force clean rebuild (clears cache) +rush test # Run all tests +``` + +## Package Structure (All packages follow this) + +``` +packages// +├── src/index.ts # Main entry (exports) +├── lib/ # Compiled JS (gitignored) +├── types/ # TS declarations (gitignored) +├── package.json # Scope: @hcengineering/, main: lib/index.js +├── tsconfig.json # Extends platform-rig +└── jest.config.js # preset: 'ts-jest', roots: ['./src'] +``` + +## Key Architecture Patterns + +**Plugin System**: `@hcengineering/platform` provides dependency injection. Packages register resources/services via plugin manifests. Example: `packages/core/src/plugin.ts` + +**Data Flow**: `core` → abstract models (Doc, Ref, Class) → `client` → concrete implementations → WebSocket/REST via `api-client` + +**Text Processing**: Modular `text-*` packages (core/html/markdown/ydoc) support extensible rich-text editing with Yjs collaboration + +**Storage Abstraction**: `storage` package defines interfaces; `storage-client` provides implementations; backends are pluggable + +## Common Tasks + +**Add a package**: Create under `packages/`, add standard files (see structure above), register in `rush.json` if needed, run `rush update` + +**Debug build failures**: Check `/rush-logs/` and `.build/build.tsbuildinfo`; use `rush rebuild` to clear incremental state + +**Run single package tests**: `cd packages/ && rushx test` + +**Update dependencies**: Edit package.json, run `rush update`, verify with `rush build` + +## Where to Look + +- **Rush config**: `common/config/rush/`, `rush.json` +- **Shared scripts**: `common/scripts/` (coverage merging, install helpers) +- **Core abstractions**: `packages/core/src/` (Doc, Hierarchy, TxOperations) +- **Platform runtime**: `packages/platform/src/` (plugin loader, resources) +- **Client layer**: `packages/client/src/` (LiveQuery, TxOperations client wrapper) + +## Watch Out For + +- Rush uses **incremental builds** - cached artifacts in `.build/` and `common/temp/` +- **TypeScript strict mode** enabled - type safety enforced +- **platform-rig** centralizes tooling configs (ESLint, Prettier, tsconfig) +- Check `common/temp/rush-recycler/` for moved files during dependency updates diff --git a/foundations/core/.github/workflows/ci.yml b/foundations/core/.github/workflows/ci.yml new file mode 100644 index 0000000000..81ca764570 --- /dev/null +++ b/foundations/core/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: ['main'] + tags: + - 'v0.7.*' + - 's0.7.*' + pull_request: + branches: ['main'] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + - uses: actions/setup-node@v3 + with: + node-version: 22 + - name: Verify Change Logs + run: node common/scripts/install-run-rush.js change --verify + - name: Rush Install + run: node common/scripts/install-run-rush.js install + - name: Rush check + run: node common/scripts/install-run-rush.js check --verbose + - name: Rush validate + run: node common/scripts/install-run-rush.js validate --verbose + - name: Rush test + run: node common/scripts/install-run-rush.js test --verbose + - name: Publish packages + - name: Formatting... + run: node common/scripts/install-run-rush.js format --force + - name: Check files formatting + run: | + echo '================================================================' + echo 'Checking for diff files' + echo '================================================================' + git diff '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat + [ -z "$(git diff --name-only '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat)" ] + echo '================================================================' + if: startsWith(github.ref, 'refs/tags/v0.7.') || startsWith(github.ref, 'refs/tags/s0.7.') + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: node common/scripts/install-run-rush.js publish --include-all --publish diff --git a/foundations/core/.gitignore b/foundations/core/.gitignore new file mode 100644 index 0000000000..a175778200 --- /dev/null +++ b/foundations/core/.gitignore @@ -0,0 +1,115 @@ +.heft/ +lib/ +_api-extractor-temp/ +temp/ +.idea +pods/workspace/init/ +pods/workspace/init-scripts/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +*./rush-logs +*tests/sanity/screenshots + +# Runtime data +*.pid +*.seed +*.pid.lock + +# VS Code settings +.vscode/settings.json + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +# .env + +# next.js build output +.next + +# OS X temporary files +.DS_Store + +# Rush temporary files +common/deploy/ +common/temp/ +common/autoinstallers/*/.npmrc +**/.rush/temp/ +bundle.js +bundle/*.js +dist +.build +typings +types +.validate +tsconfig.tsbuildinfo +ingest-attachment-*.zip +tsdoc-metadata.json +pods/front/dist +*.cpuprofile +*.pyc +metrics.txt +dev/tool/report*.csv +tests/db_dump +.build +.format +tools/apm/apm.js +deploy +metrics.txt +services/github/pod-github/src/github.graphql +.build +.format +dev/tool/report.csv +bundle/* +bundle.js.map +tests/profiles +**/bundle/model.json +.wrangler +dump +**/logs/** +dev/tool/history.json +.aider* +/combined_dependencies +.tmp +ws-tests/docker-compose.override.yml diff --git a/foundations/core/.nvmrc b/foundations/core/.nvmrc new file mode 100644 index 0000000000..53d1c14db3 --- /dev/null +++ b/foundations/core/.nvmrc @@ -0,0 +1 @@ +v22 diff --git a/foundations/core/.prettierrc b/foundations/core/.prettierrc new file mode 100644 index 0000000000..00d5897747 --- /dev/null +++ b/foundations/core/.prettierrc @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "trailingComma": "none", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 120, + "useTabs": false, + "bracketSpacing": true, + "proseWrap": "preserve", + "plugins": [], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/foundations/core/.vscode/extensions.json b/foundations/core/.vscode/extensions.json new file mode 100644 index 0000000000..b8552f9c56 --- /dev/null +++ b/foundations/core/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "svelte.svelte-vscode", + "esbenp.prettier-vscode", + "firsttris.vscode-jest-runner" + ] +} diff --git a/foundations/core/LICENSE b/foundations/core/LICENSE new file mode 100644 index 0000000000..e48e096345 --- /dev/null +++ b/foundations/core/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/foundations/core/README.md b/foundations/core/README.md new file mode 100644 index 0000000000..0221a7792b --- /dev/null +++ b/foundations/core/README.md @@ -0,0 +1,167 @@ +# Huly Core + +[![GitHub License](https://img.shields.io/github/license/hcengineering/huly.core?style=for-the-badge)](LICENSE) + +⭐️ Your star shines on us. Star us on GitHub! + +## About + +Huly Core is a collection of core packages extracted from the [Huly Platform](https://github.com/hcengineering/platform). This repository contains fundamental building blocks and libraries that power the Huly ecosystem, including core data models, client libraries, text processing engines, and platform utilities. + +These packages are designed to be reusable, modular, and framework-agnostic, making them suitable for building custom applications on top of the Huly Platform or integrating Huly functionality into existing projects. + +## Packages + +This repository includes the following core packages: + +### Core Packages + +- **[@hcengineering/core](packages/core)** - Core data models, types, and fundamental platform abstractions +- **[@hcengineering/platform](packages/platform)** - Platform runtime, plugin system, and dependency injection +- **[@hcengineering/model](packages/model)** - Data model definitions and schema management + +### Client Libraries + +- **[@hcengineering/client](packages/client)** - Client-side data access and synchronization layer +- **[@hcengineering/client-resources](packages/client-resources)** - Shared client resources and utilities +- **[@hcengineering/api-client](packages/api-client)** - API client for programmatic access to Huly Platform (WebSocket and REST) +- **[@hcengineering/account-client](packages/account-client)** - Account management client +- **[@hcengineering/collaborator-client](packages/collaborator-client)** - Real-time collaboration client +- **[@hcengineering/hulylake-client](packages/hulylake-client)** - HulyLake data warehouse client +- **[@hcengineering/analytics](packages/analytics)** - Analytics and tracking +- **[@hcengineering/analytics-service](packages/analytics-service)** - Analytics service implementation + +### Text Processing + +- **[@hcengineering/text](packages/text)** - High-level text processing utilities +- **[@hcengineering/text-core](packages/text-core)** - Core text processing engine +- **[@hcengineering/text-html](packages/text-html)** - HTML text rendering and parsing +- **[@hcengineering/text-markdown](packages/text-markdown)** - Markdown support +- **[@hcengineering/text-ydoc](packages/text-ydoc)** - Yjs document integration for collaborative editing + +### Utilities + +- **[@hcengineering/query](packages/query)** - Query language and execution engine +- **[@hcengineering/storage](packages/storage)** - Storage abstractions and implementations +- **[@hcengineering/rank](packages/rank)** - Ranking and ordering utilities +- **[@hcengineering/retry](packages/retry)** - Retry logic and resilience patterns +- **[@hcengineering/rpc](packages/rpc)** - RPC communication layer +- **[@hcengineering/token](packages/token)** - Token management and authentication utilities + +## Pre-requisites + +Before proceeding, ensure that your system meets the following requirements: + +- [Node.js](https://nodejs.org/en/download/) (v20.11.0 or higher is required) +- [Rush](https://rushjs.io/) - Microsoft's scalable monorepo manager + +## Installation + +You need Microsoft's [rush](https://rushjs.io/) to install the application. + +1. Install Rush globally using the command: + +```bash +npm install -g @microsoft/rush +``` + +1. Navigate to the repository root and run the following commands: + +```bash +rush install +rush build +``` + +## Build + +To build all packages: + +```bash +rush build +``` + +To rebuild (ignoring cache): + +```bash +rush rebuild +``` + +## Build & Watch + +For development purposes, `rush build:watch` action could be used: + +```bash +rush build:watch +``` + +It includes build and validate phases in watch mode. + +## Update project structure + +If the project's structure is updated, it may be necessary to relink and rebuild the projects: + +```bash +rush update +rush build +``` + +## Troubleshooting + +If a build fails, but the code is correct, try to delete the [build cache](https://rushjs.io/pages/maintainer/build_cache/) and retry: + +```bash +rm -rf common/temp/build-cache +rush rebuild +``` + +## Tests + +To execute all tests: + +```bash +rush test +``` + +For individual test execution inside a package directory: + +```bash +rushx test +``` + +## Package Publishing + +To bump a package version: + +```bash +node ./common/scripts/bump.js -p projectName +``` + +## API Client Usage + +If you want to interact with Huly programmatically, check out the [API Client](packages/api-client/README.md) documentation. The API client provides a typed interface for all Huly operations and can be used to build integrations and custom applications. + +You can find API usage examples in the [Huly examples](https://github.com/hcengineering/huly-examples) repository. + +## Related Projects + +- **[Huly Platform](https://github.com/hcengineering/platform)** - The main Huly Platform repository +- **[Huly Self-Host](https://github.com/hcengineering/huly-selfhost)** - Self-hosting solution for Huly +- **[Huly Examples](https://github.com/hcengineering/huly-examples)** - API usage examples + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +Licensed under the [EPL-2.0](LICENSE) license. + +## Additional Links + +- [Huly Website](https://huly.io/) +- [Documentation](https://docs.huly.io/) +- [Community](https://github.com/hcengineering/platform/discussions) + +--- + +© 2025 [Hardcore Engineering Inc](https://hardcoreeng.com/). diff --git a/foundations/core/common/changes/@hcengineering/client-resources/enable-formatting-check_2025-11-20-14-13.json b/foundations/core/common/changes/@hcengineering/client-resources/enable-formatting-check_2025-11-20-14-13.json new file mode 100644 index 0000000000..77ca2b0fb1 --- /dev/null +++ b/foundations/core/common/changes/@hcengineering/client-resources/enable-formatting-check_2025-11-20-14-13.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@hcengineering/client-resources", + "comment": "Add support for custom exclude filters", + "type": "patch" + } + ], + "packageName": "@hcengineering/client-resources" +} \ No newline at end of file diff --git a/foundations/core/common/changes/@hcengineering/client-resources/main_2025-10-29-07-46.json b/foundations/core/common/changes/@hcengineering/client-resources/main_2025-10-29-07-46.json new file mode 100644 index 0000000000..2c0564f3c0 --- /dev/null +++ b/foundations/core/common/changes/@hcengineering/client-resources/main_2025-10-29-07-46.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@hcengineering/client-resources", + "comment": "formatting", + "type": "none" + } + ], + "packageName": "@hcengineering/client-resources" +} \ No newline at end of file diff --git a/foundations/core/common/changes/@hcengineering/client/enable-formatting-check_2025-11-20-14-13.json b/foundations/core/common/changes/@hcengineering/client/enable-formatting-check_2025-11-20-14-13.json new file mode 100644 index 0000000000..508a372f52 --- /dev/null +++ b/foundations/core/common/changes/@hcengineering/client/enable-formatting-check_2025-11-20-14-13.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@hcengineering/client", + "comment": "Add support for custom exclude filters", + "type": "patch" + } + ], + "packageName": "@hcengineering/client" +} \ No newline at end of file diff --git a/foundations/core/common/config/rush/.npmrc b/foundations/core/common/config/rush/.npmrc new file mode 100644 index 0000000000..4bb3c57a4e --- /dev/null +++ b/foundations/core/common/config/rush/.npmrc @@ -0,0 +1,33 @@ +# Rush uses this file to configure the NPM package registry during installation. It is applicable +# to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", +# "rush update", and the "install-run.js" scripts. +# +# NOTE: The "rush publish" command uses .npmrc-publish instead. +# +# Before invoking the package manager, Rush will generate an .npmrc in the folder where installation +# is performed. This generated file will omit any config lines that reference environment variables +# that are undefined in that session; this avoids problems that would otherwise result due to +# a missing variable being replaced by an empty string. +# +# If "subspacesEnabled" is true in subspaces.json, the generated file will merge settings from +# "common/config/rush/.npmrc" and "common/config/subspaces//.npmrc", with the latter taking +# precedence. +# +# * * * SECURITY WARNING * * * +# +# It is NOT recommended to store authentication tokens in a text file on a lab machine, because +# other unrelated processes may be able to read that file. Also, the file may persist indefinitely, +# for example if the machine loses power. A safer practice is to pass the token via an +# environment variable, which can be referenced from .npmrc using ${} expansion. For example: +# +# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} +# + +# Explicitly specify the NPM registry that "rush install" and "rush update" will use by default: +registry=https://registry.npmjs.org/ + +# Optionally provide an authentication token for the above registry URL (if it is a private registry): +//registry.npmjs.org/:_authToken=${NPM_TOKEN} + +# Change this to "true" if your registry requires authentication for read-only operations: +always-auth=false diff --git a/foundations/core/common/config/rush/.npmrc-publish b/foundations/core/common/config/rush/.npmrc-publish new file mode 100644 index 0000000000..951a4918cf --- /dev/null +++ b/foundations/core/common/config/rush/.npmrc-publish @@ -0,0 +1,29 @@ +# This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish +# is used by the "rush publish" command, as publishing often involves different credentials +# and registries than other operations. +# +# Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc" +# and then temporarily map that folder as the "home directory" for the current user account. +# This enables the same settings to apply for each project folder that gets published. The copied file +# will omit any config lines that reference environment variables that are undefined in that session; +# this avoids problems that would otherwise result due to a missing variable being replaced by +# an empty string. +# +# * * * SECURITY WARNING * * * +# +# It is NOT recommended to store authentication tokens in a text file on a lab machine, because +# other unrelated processes may be able to read the file. Also, the file may persist indefinitely, +# for example if the machine loses power. A safer practice is to pass the token via an +# environment variable, which can be referenced from .npmrc using ${} expansion. For example: +# +# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} +# + +# Explicitly specify the NPM registry that "rush publish" will use by default: +registry=https://registry.npmjs.org/ + +# Optionally provide an authentication token for the above registry URL (if it is a private registry): +//registry.npmjs.org/:_authToken=${NPM_TOKEN} + +# Change this to "true" if your registry requires authentication for read-only operations: +always-auth=false diff --git a/foundations/core/common/config/rush/.pnpmfile.cjs b/foundations/core/common/config/rush/.pnpmfile.cjs new file mode 100644 index 0000000000..98cf3279ec --- /dev/null +++ b/foundations/core/common/config/rush/.pnpmfile.cjs @@ -0,0 +1,38 @@ +'use strict'; + +/** + * When using the PNPM package manager, you can use pnpmfile.js to workaround + * dependencies that have mistakes in their package.json file. (This feature is + * functionally similar to Yarn's "resolutions".) + * + * For details, see the PNPM documentation: + * https://pnpm.io/pnpmfile#hooks + * + * IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY TO INVALIDATE + * ANY CACHED DEPENDENCY ANALYSIS. After any modification to pnpmfile.js, it's recommended to run + * "rush update --full" so that PNPM will recalculate all version selections. + */ +module.exports = { + hooks: { + readPackage + } +}; + +/** + * This hook is invoked during installation before a package's dependencies + * are selected. + * The `packageJson` parameter is the deserialized package.json + * contents for the package that is about to be installed. + * The `context` parameter provides a log() function. + * The return value is the updated object. + */ +function readPackage(packageJson, context) { + + // // The karma types have a missing dependency on typings from the log4js package. + // if (packageJson.name === '@types/karma') { + // context.log('Fixed up dependencies for @types/karma'); + // packageJson.dependencies['log4js'] = '0.6.38'; + // } + + return packageJson; +} diff --git a/foundations/core/common/config/rush/artifactory.json b/foundations/core/common/config/rush/artifactory.json new file mode 100644 index 0000000000..268065478a --- /dev/null +++ b/foundations/core/common/config/rush/artifactory.json @@ -0,0 +1,109 @@ +/** + * This configuration file manages Rush integration with JFrog Artifactory services. + * More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/artifactory.schema.json", + + "packageRegistry": { + /** + * (Required) Set this to "true" to enable Rush to manage tokens for an Artifactory NPM registry. + * When enabled, "rush install" will automatically detect when the user's ~/.npmrc + * authentication token is missing or expired. And "rush setup" will prompt the user to + * renew their token. + * + * The default value is false. + */ + "enabled": false, + + /** + * (Required) Specify the URL of your NPM registry. This is the same URL that appears in + * your .npmrc file. It should look something like this example: + * + * https://your-company.jfrog.io/your-project/api/npm/npm-private/ + */ + "registryUrl": "", + + /** + * A list of custom strings that "rush setup" should add to the user's ~/.npmrc file at the time + * when the token is updated. This could be used for example to configure the company registry + * to be used whenever NPM is invoked as a standalone command (but it's not needed for Rush + * operations like "rush add" and "rush install", which get their mappings from the monorepo's + * common/config/rush/.npmrc file). + * + * NOTE: The ~/.npmrc settings are global for the user account on a given machine, so be careful + * about adding settings that may interfere with other work outside the monorepo. + */ + "userNpmrcLinesToAdd": [ + // "@example:registry=https://your-company.jfrog.io/your-project/api/npm/npm-private/" + ], + + /** + * (Required) Specifies the URL of the Artifactory control panel where the user can generate + * an API key. This URL is printed after the "visitWebsite" message. + * It should look something like this example: https://your-company.jfrog.io/ + * Specify an empty string to suppress this line entirely. + */ + "artifactoryWebsiteUrl": "", + + /** + * Uncomment this line to specify the type of credential to save in the user's ~/.npmrc file. + * The default is "password", which means the user's API token will be traded in for an + * npm password specific to that registry. Optionally you can specify "authToken", which + * will save the user's API token as credentials instead. + */ + // "credentialType": "password", + + /** + * These settings allow the "rush setup" interactive prompts to be customized, for + * example with messages specific to your team or configuration. Specify an empty string + * to suppress that message entirely. + */ + "messageOverrides": { + /** + * Overrides the message that normally says: + * "This monorepo consumes packages from an Artifactory private NPM registry." + */ + // "introduction": "", + + /** + * Overrides the message that normally says: + * "Please contact the repository maintainers for help with setting up an Artifactory user account." + */ + // "obtainAnAccount": "", + + /** + * Overrides the message that normally says: + * "Please open this URL in your web browser:" + * + * The "artifactoryWebsiteUrl" string is printed after this message. + */ + // "visitWebsite": "", + + /** + * Overrides the message that normally says: + * "Your user name appears in the upper-right corner of the JFrog website." + */ + // "locateUserName": "", + + /** + * Overrides the message that normally says: + * "Click 'Edit Profile' on the JFrog website. Click the 'Generate API Key' + * button if you haven't already done so previously." + */ + // "locateApiKey": "" + + /** + * Overrides the message that normally prompts: + * "What is your Artifactory user name?" + */ + // "userNamePrompt": "" + + /** + * Overrides the message that normally prompts: + * "What is your Artifactory API key?" + */ + // "apiKeyPrompt": "" + } + } +} diff --git a/foundations/core/common/config/rush/build-cache.json b/foundations/core/common/config/rush/build-cache.json new file mode 100644 index 0000000000..072e9f7d49 --- /dev/null +++ b/foundations/core/common/config/rush/build-cache.json @@ -0,0 +1,160 @@ +/** + * This configuration file manages Rush's build cache feature. + * More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json", + + /** + * (Required) EXPERIMENTAL - Set this to true to enable the build cache feature. + * + * See https://rushjs.io/pages/maintainer/build_cache/ for details about this experimental feature. + */ + "buildCacheEnabled": false, + + /** + * (Required) Choose where project build outputs will be cached. + * + * Possible values: "local-only", "azure-blob-storage", "amazon-s3" + */ + "cacheProvider": "local-only", + + /** + * Setting this property overrides the cache entry ID. If this property is set, it must contain + * a [hash] token. + * + * Other available tokens: + * - [projectName] Example: "@my-scope/my-project" + * - [projectName:normalize] Example: "my-scope+my-project" + * - [phaseName] Example: "_phase:test/api" + * - [phaseName:normalize] Example: "_phase:test+api" + * - [phaseName:trimPrefix] Example: "test/api" + * - [os] Example: "win32" + * - [arch] Example: "x64" + */ + // "cacheEntryNamePattern": "[projectName:normalize]-[phaseName:normalize]-[hash]" + + /** + * (Optional) Salt to inject during calculation of the cache key. This can be used to invalidate the cache for all projects when the salt changes. + */ + // "cacheHashSalt": "1", + + /** + * Use this configuration with "cacheProvider"="azure-blob-storage" + */ + "azureBlobStorageConfiguration": { + /** + * (Required) The name of the the Azure storage account to use for build cache. + */ + // "storageAccountName": "example", + + /** + * (Required) The name of the container in the Azure storage account to use for build cache. + */ + // "storageContainerName": "my-container", + + /** + * The Azure environment the storage account exists in. Defaults to AzurePublicCloud. + * + * Possible values: "AzurePublicCloud", "AzureChina", "AzureGermany", "AzureGovernment" + */ + // "azureEnvironment": "AzurePublicCloud", + + /** + * An optional prefix for cache item blob names. + */ + // "blobPrefix": "my-prefix", + + /** + * If set to true, allow writing to the cache. Defaults to false. + */ + // "isCacheWriteAllowed": true, + + /** + * The Entra ID login flow to use. Defaults to 'AdoCodespacesAuth' on GitHub Codespaces, 'InteractiveBrowser' otherwise. + */ + // "loginFlow": "InteractiveBrowser", + + /** + * If set to true, reading the cache requires authentication. Defaults to false. + */ + // "readRequiresAuthentication": true + }, + + /** + * Use this configuration with "cacheProvider"="amazon-s3" + */ + "amazonS3Configuration": { + /** + * (Required unless s3Endpoint is specified) The name of the bucket to use for build cache. + * Example: "my-bucket" + */ + // "s3Bucket": "my-bucket", + + /** + * (Required unless s3Bucket is specified) The Amazon S3 endpoint of the bucket to use for build cache. + * This should not include any path; use the s3Prefix to set the path. + * Examples: "my-bucket.s3.us-east-2.amazonaws.com" or "http://localhost:9000" + */ + // "s3Endpoint": "https://my-bucket.s3.us-east-2.amazonaws.com", + + /** + * (Required) The Amazon S3 region of the bucket to use for build cache. + * Example: "us-east-1" + */ + // "s3Region": "us-east-1", + + /** + * An optional prefix ("folder") for cache items. It should not start with "/". + */ + // "s3Prefix": "my-prefix", + + /** + * If set to true, allow writing to the cache. Defaults to false. + */ + // "isCacheWriteAllowed": true + }, + + /** + * Use this configuration with "cacheProvider"="http" + */ + "httpConfiguration": { + /** + * (Required) The URL of the server that stores the caches. + * Example: "https://build-cacches.example.com/" + */ + // "url": "https://build-cacches.example.com/", + + /** + * (Optional) The HTTP method to use when writing to the cache (defaults to PUT). + * Should be one of PUT, POST, or PATCH. + * Example: "PUT" + */ + // "uploadMethod": "PUT", + + /** + * (Optional) HTTP headers to pass to the cache server. + * Example: { "X-HTTP-Company-Id": "109283" } + */ + // "headers": {}, + + /** + * (Optional) Shell command that prints the authorization token needed to communicate with the + * cache server, and exits with exit code 0. This command will be executed from the root of + * the monorepo. + * Example: { "exec": "node", "args": ["common/scripts/auth.js"] } + */ + // "tokenHandler": { "exec": "node", "args": ["common/scripts/auth.js"] }, + + /** + * (Optional) Prefix for cache keys. + * Example: "my-company-" + */ + // "cacheKeyPrefix": "", + + /** + * (Optional) If set to true, allow writing to the cache. Defaults to false. + */ + // "isCacheWriteAllowed": true + } +} diff --git a/foundations/core/common/config/rush/cobuild.json b/foundations/core/common/config/rush/cobuild.json new file mode 100644 index 0000000000..a47fad18d5 --- /dev/null +++ b/foundations/core/common/config/rush/cobuild.json @@ -0,0 +1,22 @@ +/** + * This configuration file manages Rush's cobuild feature. + * More documentation is available on the Rush website: https://rushjs.io + */ + { + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/cobuild.schema.json", + + /** + * (Required) EXPERIMENTAL - Set this to true to enable the cobuild feature. + * RUSH_COBUILD_CONTEXT_ID should always be specified as an environment variable with an non-empty string, + * otherwise the cobuild feature will be disabled. + */ + "cobuildFeatureEnabled": false, + + /** + * (Required) Choose where cobuild lock will be acquired. + * + * The lock provider is registered by the rush plugins. + * For example, @rushstack/rush-redis-cobuild-plugin registers the "redis" lock provider. + */ + "cobuildLockProvider": "redis" +} diff --git a/foundations/core/common/config/rush/command-line.json b/foundations/core/common/config/rush/command-line.json new file mode 100644 index 0000000000..edf0062005 --- /dev/null +++ b/foundations/core/common/config/rush/command-line.json @@ -0,0 +1,517 @@ +/** + * This configuration file defines custom commands for the "rush" command-line. + * More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", + + "phases": [ + { + "name": "_phase:build", + "dependencies": { + "upstream": ["_phase:build"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": false + }, + { + "name": "_phase:validate", + "dependencies": { + "self": ["_phase:build"], + "upstream": ["_phase:validate", "_phase:build"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": false + }, + { + "name": "_phase:test", + "dependencies": { + "self": ["_phase:build"], + "upstream": ["_phase:validate"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": true + }, + { + "name": "_phase:lint", + "dependencies": { + "self": ["_phase:build"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": false + }, + { + "name": "_phase:bundle", + "dependencies": { + "self": ["_phase:build"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": false + }, + { + "name": "_phase:format", + "dependencies": { + "self": ["_phase:build"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": false + }, + { + "name": "_phase:svelte-check", + "dependencies": { + "self": ["_phase:build"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": true + }, + { + "name": "_phase:package", + "dependencies": { + "self": ["_phase:build"], + "upstream": ["_phase:package"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": false + }, + { + "name": "_phase:docker-build", + "dependencies": { + "self": ["_phase:build", "_phase:package", "_phase:bundle"], + "upstream": ["_phase:build", "_phase:bundle", "_phase:package"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": true + }, + { + "name": "_phase:docker-staging", + "dependencies": { + "self": ["_phase:build", "_phase:package", "_phase:bundle"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": true + } + ], + "commands": [ + { + "commandKind": "global", + "name": "coverage", + "summary": "Run tests, merge LCOV and generate HTML coverage", + "description": "Run 'rush test', then merge per-package LCOV files and generate HTML coverage in coverage/html", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "rush test && node scripts/merge-coverage.js && node scripts/generate-coverage-html.js coverage/lcov.info coverage/html" + }, + { + "commandKind": "bulk", + "name": "format", + "summary": "Format", + "description": "Perform a formatting", + "enableParallelism": true, + "incremental": false, + "ignoreMissingScript": true, + "safeForSimultaneousRushProcesses": true, + "disableBuildCache": true + }, + { + "commandKind": "global", + "name": "doformat", + "summary": "Do a format and show errors after it", + "description": "Do a format and show errors after it", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "(rush format && true) && ./common/scripts/format-show.sh" + }, + { + "commandKind": "phased", + "name": "build:watch", + "summary": "Build and watch", + "phases": ["_phase:build", "_phase:validate"], + "description": "Perform build with tsc and watch for changes with rush", + "enableParallelism": true, + "incremental": true, + "safeForSimultaneousRushProcesses": true, + "watchOptions": { + "alwaysWatch": true, + "watchPhases": ["_phase:build", "_phase:validate"] + } + }, + { + "summary": "svelte-check", + "commandKind": "phased", + "name": "svelte-check", + "phases": ["_phase:build", "_phase:svelte-check"], + "enableParallelism": true, + "incremental": true + }, + { + "commandKind": "phased", + "name": "build", + "summary": "build", + "phases": ["_phase:build"], + "enableParallelism": true, + "incremental": true, + "watchOptions": { + "alwaysWatch": false, + "watchPhases": ["_phase:build"] + } + }, + { + "commandKind": "phased", + "name": "validate", + "phases": ["_phase:validate"], + "summary": "validate", + "enableParallelism": true, + "incremental": true + }, + { + "commandKind": "phased", + "name": "rebuild", + "summary": "ReBuild and test all projects.", + "phases": ["_phase:build"], + "enableParallelism": true, + "incremental": false + }, + { + "commandKind": "phased", + "name": "dorevalidate", + "phases": ["_phase:validate"], + "summary": "dorevalidate", + "enableParallelism": true, + "incremental": false + }, + { + "commandKind": "phased", + "summary": "Do testing", + "name": "test", + "phases": ["_phase:build", "_phase:test"], + "enableParallelism": true, + "incremental": true + }, + { + "commandKind": "phased", + "name": "retest", + "summary": "Build and test all projects.", + "phases": ["_phase:build", "_phase:test"], + "enableParallelism": true, + "incremental": false + }, + + { + "commandKind": "phased", + "summary": "Do bundle", + "name": "bundle", + "phases": ["_phase:build", "_phase:bundle"], + "enableParallelism": true, + "incremental": true + }, + { + "commandKind": "phased", + "summary": "Do packaging", + "name": "package", + "phases": ["_phase:build", "_phase:package"], + "enableParallelism": true, + "incremental": true + }, + + { + "summary": "docker:build", + "commandKind": "phased", + "name": "docker:build", + "phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-build"], + "enableParallelism": true, + "incremental": true + }, + { + "summary": "docker:rebuild", + "commandKind": "phased", + "name": "docker:rebuild", + "phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-build"], + "enableParallelism": true, + "incremental": false + }, + { + "summary": "docker:staging", + "commandKind": "phased", + "name": "docker:staging", + "phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-staging"], + "enableParallelism": true, + "incremental": true + }, + { + "commandKind": "bulk", + "name": "docker:push", + "summary": "docker:push", + "description": "Push docker release images", + "enableParallelism": true, + "incremental": false, + "ignoreDependencyOrder": false, + "ignoreMissingScript": true, + "disableBuildCache": true, + "allowWarningsInSuccessfulBuild": true + }, + { + "commandKind": "global", + "name": "docker", + "summary": "Build docker with platform", + "description": "use to build all docker containers required for platform", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "./common/scripts/docker.sh" + }, + { + "commandKind": "global", + "name": "docker:up", + "summary": "Up development build", + "description": "Up development build", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "cd ./dev && docker compose up -d --force-recreate" + }, + { + "commandKind": "global", + "name": "docker:local", + "summary": "Up development build", + "description": "Up development build", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "cd ./dev/local-mongo && docker compose -p dev up -d --force-recreate" + }, + { + "commandKind": "global", + "name": "tool:upgrade", + "summary": "Upgrade all local models", + "description": "Upgrade all local models", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "cd ./dev/tool && rushx run-local upgrade -f" + }, + { + "commandKind": "global", + "name": "apply-templates", + "summary": "Update all package.json according to templates matched from templates folder", + "description": "Use to update all projects to templates", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "node templates/apply.js" + }, + { + "commandKind": "global", + "name": "ts-clean", + "summary": "Clean tsconfig.tsbuildinfo", + "description": "Clean typescript incremental cache", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "find .|grep tsconfig.tsbuildinfo | xargs rm | pwd" + }, + { + "commandKind": "global", + "name": "revalidate", + "summary": "Clean Validate cache and to validate again", + "description": "Clean typescript incremental cache", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "find .|grep tsBuildInfoFile.info | xargs rm | pwd && rush dorevalidate" + }, + { + "commandKind": "bulk", + "name": "remove-ts-types", + "summary": "Clean validate types", + "description": "Clean validate types", + "shellCommand": "rm -rf ./types", + "enableParallelism": true, + "incremental": false, + "ignoreDependencyOrder": true, + "ignoreMissingScript": true, + "disableBuildCache": true, + "allowWarningsInSuccessfulBuild": true + }, + { + "commandKind": "global", + "name": "model-version", + "summary": "show model version", + "description": "show model version", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "npx node ./common/scripts/show_version.js" + }, + { + "commandKind": "global", + "name": "show-model", + "summary": "show model", + "description": "show model", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "cd ./models/all && rushx show-model" + }, + { + "commandKind": "global", + "name": "deps-clean", + "summary": "Clean package-deps-*.json files", + "description": "Clean package-deps-*.json files", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "find .|grep .rush/temp/package-deps_ | xargs rm" + }, + { + "commandKind": "global", + "name": "fast-format", + "summary": "Format changed projects", + "description": "Format and autofix linting issues in changed projects", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "./common/scripts/fast-format.sh" + }, + { + "commandKind": "global", + "name": "bump-changes", + "summary": "Bump changes from tag", + "description": "Bump changes from previous tag", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "./common/scripts/node_modules/.bin/bump-changes-from-tag" + } + ], + + /** + * Custom "parameters" introduce new parameters for specified Rush command-line commands. + * For example, you might define a "--production" parameter for the "rush build" command. + */ + "parameters": [ + { + "parameterKind": "flag", + "longName": "--lite", + "shortName": "-l", + "description": "Enable Heft lite building option, will skip some phases.", + "associatedCommands": ["build", "rebuild"] + }, + { + "parameterKind": "flag", + "longName": "--clean", + "description": "Enable Heft clean building option", + "associatedCommands": ["build", "rebuild"] + }, + { + "parameterKind": "flag", + "longName": "--force", + "shortName": "-f", + "description": "Force formatting", + "associatedCommands": ["format"] + }, + { + "parameterKind": "string", + "argumentName": "BRANCH", + "required": false, + "associatedPhases": [], + "shortName": "-b", + "longName": "--branch", + "description": "Force formatting of branch", + "associatedCommands": ["fast-format"] + } + // { + // /** + // * (Required) Determines the type of custom parameter. + // * A "flag" is a custom command-line parameter whose presence acts as an on/off switch. + // */ + // "parameterKind": "flag", + // + // /** + // * (Required) The long name of the parameter. It must be lower-case and use dash delimiters. + // */ + // "longName": "--my-flag", + // + // /** + // * An optional alternative short name for the parameter. It must be a dash followed by a single + // * lower-case or upper-case letter, which is case-sensitive. + // * + // * NOTE: The Rush developers recommend that automation scripts should always use the long name + // * to improve readability. The short name is only intended as a convenience for humans. + // * The alphabet letters run out quickly, and are difficult to memorize, so *only* use + // * a short name if you expect the parameter to be needed very often in everyday operations. + // */ + // "shortName": "-m", + // + // /** + // * (Required) A long description to be shown in the command-line help. + // * + // * Whenever you introduce commands/parameters, taking a little time to write meaningful + // * documentation can make a big difference for the developer experience in your repo. + // */ + // "description": "A custom flag parameter that is passed to the scripts that are invoked when building projects", + // + // /** + // * (Required) A list of custom commands and/or built-in Rush commands that this parameter may + // * be used with. The parameter will be appended to the shell command that Rush invokes. + // */ + // "associatedCommands": ["build", "rebuild"] + // }, + // + // { + // /** + // * (Required) Determines the type of custom parameter. + // * A "string" is a custom command-line parameter whose value is a simple text string. + // */ + // "parameterKind": "string", + // "longName": "--my-string", + // "description": "A custom string parameter for the \"my-global-command\" custom command", + // + // "associatedCommands": ["my-global-command"], + // + // /** + // * The name of the argument, which will be shown in the command-line help. + // * + // * For example, if the parameter name is '--count" and the argument name is "NUMBER", + // * then the command-line help would display "--count NUMBER". The argument name must + // * be comprised of upper-case letters, numbers, and underscores. It should be kept short. + // */ + // "argumentName": "SOME_TEXT", + // + // /** + // * If true, this parameter must be included with the command. The default is false. + // */ + // "required": false + // }, + // + // { + // /** + // * (Required) Determines the type of custom parameter. + // * A "choice" is a custom command-line parameter whose argument must be chosen from a list of + // * allowable alternatives. + // */ + // "parameterKind": "choice", + // "longName": "--my-choice", + // "description": "A custom choice parameter for the \"my-global-command\" custom command", + // + // "associatedCommands": ["my-global-command"], + // + // /** + // * If true, this parameter must be included with the command. The default is false. + // */ + // "required": false, + // + // /** + // * Normally if a parameter is omitted from the command line, it will not be passed + // * to the shell command. this value will be inserted by default. Whereas if a "defaultValue" + // * is defined, the parameter will always be passed to the shell command, and will use the + // * default value if unspecified. The value must be one of the defined alternatives. + // */ + // "defaultValue": "vanilla", + // + // /** + // * (Required) A list of alternative argument values that can be chosen for this parameter. + // */ + // "alternatives": [ + // { + // /** + // * A token that is one of the alternatives that can be used with the choice parameter, + // * e.g. "vanilla" in "--flavor vanilla". + // */ + // "name": "vanilla", + // + // /** + // * A detailed description for the alternative that can be shown in the command-line help. + // * + // * Whenever you introduce commands/parameters, taking a little time to write meaningful + // * documentation can make a big difference for the developer experience in your repo. + // */ + // "description": "Use the vanilla flavor (the default)" + // }, + // + // { + // "name": "chocolate", + // "description": "Use the chocolate flavor" + // }, + // + // { + // "name": "strawberry", + // "description": "Use the strawberry flavor" + // } + // ] + // } + ] +} diff --git a/foundations/core/common/config/rush/common-versions.json b/foundations/core/common/config/rush/common-versions.json new file mode 100644 index 0000000000..4a3ecbd1ee --- /dev/null +++ b/foundations/core/common/config/rush/common-versions.json @@ -0,0 +1,77 @@ +/** + * This configuration file specifies NPM dependency version selections that affect all projects + * in a Rush repo. More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", + + /** + * A table that specifies a "preferred version" for a given NPM package. This feature is typically used + * to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies. + * + * The "preferredVersions" value can be any SemVer range specifier (e.g. "~1.2.3"). Rush injects these values into + * the "dependencies" field of the top-level common/temp/package.json, which influences how the package manager + * will calculate versions. The specific effect depends on your package manager. Generally it will have no + * effect on an incompatible or already constrained SemVer range. If you are using PNPM, similar effects can be + * achieved using the pnpmfile.js hook. See the Rush documentation for more details. + * + * After modifying this field, it's recommended to run "rush update --full" so that the package manager + * will recalculate all version selections. + */ + "preferredVersions": { + /** + * When someone asks for "^1.0.0" make sure they get "1.2.3" when working in this repo, + * instead of the latest version. + */ + // "some-library": "1.2.3" + }, + + /** + * When set to true, for all projects in the repo, all dependencies will be automatically added as preferredVersions, + * except in cases where different projects specify different version ranges for a given dependency. For older + * package managers, this tended to reduce duplication of indirect dependencies. However, it can sometimes cause + * trouble for indirect dependencies with incompatible peerDependencies ranges. + * + * The default value is true. If you're encountering installation errors related to peer dependencies, + * it's recommended to set this to false. + * + * After modifying this field, it's recommended to run "rush update --full" so that the package manager + * will recalculate all version selections. + */ + // "implicitlyPreferredVersions": false, + + /** + * If you would like the version specifiers for your dependencies to be consistent, then + * uncomment this line. This is effectively similar to running "rush check" before any + * of the following commands: + * + * rush install, rush update, rush link, rush version, rush publish + * + * In some cases you may want this turned on, but need to allow certain packages to use a different + * version. In those cases, you will need to add an entry to the "allowedAlternativeVersions" + * section of the common-versions.json. + * + * In the case that subspaces is enabled, this setting will take effect at a subspace level. + */ + // "ensureConsistentVersions": true, + + /** + * The "rush check" command can be used to enforce that every project in the repo must specify + * the same SemVer range for a given dependency. However, sometimes exceptions are needed. + * The allowedAlternativeVersions table allows you to list other SemVer ranges that will be + * accepted by "rush check" for a given dependency. + * + * IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE + * USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO). + * This design avoids unnecessary churn in this file. + */ + "allowedAlternativeVersions": { + /** + * For example, allow some projects to use an older TypeScript compiler + * (in addition to whatever "usual" version is being used by other projects in the repo): + */ + // "typescript": [ + // "~2.4.0" + // ] + } +} \ No newline at end of file diff --git a/foundations/core/common/config/rush/custom-tips.json b/foundations/core/common/config/rush/custom-tips.json new file mode 100644 index 0000000000..31e540d6ad --- /dev/null +++ b/foundations/core/common/config/rush/custom-tips.json @@ -0,0 +1,29 @@ +/** + * This configuration file allows repo maintainers to configure extra details to be + * printed alongside certain Rush messages. More documentation is available on the + * Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/custom-tips.schema.json", + + /** + * Custom tips allow you to annotate Rush's console messages with advice tailored for + * your specific monorepo. + */ + "customTips": [ + // { + // /** + // * (REQUIRED) An identifier indicating a message that may be printed by Rush. + // * If that message is printed, then this custom tip will be shown. + // * The list of available tip identifiers can be found on this page: + // * https://rushjs.io/pages/maintainer/custom_tips/ + // */ + // "tipId": "TIP_RUSH_INCONSISTENT_VERSIONS", + // + // /** + // * (REQUIRED) The message text to be displayed for this tip. + // */ + // "message": "For additional troubleshooting information, refer this wiki article:\n\nhttps://intranet.contoso.com/docs/pnpm-mismatch" + // } + ] +} diff --git a/foundations/core/common/config/rush/experiments.json b/foundations/core/common/config/rush/experiments.json new file mode 100644 index 0000000000..01f8f8f902 --- /dev/null +++ b/foundations/core/common/config/rush/experiments.json @@ -0,0 +1,121 @@ +/** + * This configuration file allows repo maintainers to enable and disable experimental + * Rush features. More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json", + + /** + * By default, 'rush install' passes --no-prefer-frozen-lockfile to 'pnpm install'. + * Set this option to true to pass '--frozen-lockfile' instead for faster installs. + */ + // "usePnpmFrozenLockfileForRushInstall": true, + + /** + * By default, 'rush update' passes --no-prefer-frozen-lockfile to 'pnpm install'. + * Set this option to true to pass '--prefer-frozen-lockfile' instead to minimize shrinkwrap changes. + */ + // "usePnpmPreferFrozenLockfileForRushUpdate": true, + + /** + * By default, 'rush update' runs as a single operation. + * Set this option to true to instead update the lockfile with `--lockfile-only`, then perform a `--frozen-lockfile` install. + * Necessary when using the `afterAllResolved` hook in .pnpmfile.cjs. + */ + // "usePnpmLockfileOnlyThenFrozenLockfileForRushUpdate": true, + + /** + * If using the 'preventManualShrinkwrapChanges' option, restricts the hash to only include the layout of external dependencies. + * Used to allow links between workspace projects or the addition/removal of references to existing dependency versions to not + * cause hash changes. + */ + // "omitImportersFromPreventManualShrinkwrapChanges": true, + + /** + * If true, the chmod field in temporary project tar headers will not be normalized. + * This normalization can help ensure consistent tarball integrity across platforms. + */ + // "noChmodFieldInTarHeaderNormalization": true, + + /** + * If true, build caching will respect the allowWarningsInSuccessfulBuild flag and cache builds with warnings. + * This will not replay warnings from the cached build. + */ + // "buildCacheWithAllowWarningsInSuccessfulBuild": true, + + /** + * If true, build skipping will respect the allowWarningsInSuccessfulBuild flag and skip builds with warnings. + * This will not replay warnings from the skipped build. + */ + // "buildSkipWithAllowWarningsInSuccessfulBuild": true, + + /** + * If true, perform a clean install after when running `rush install` or `rush update` if the + * `.npmrc` file has changed since the last install. + */ + // "cleanInstallAfterNpmrcChanges": true, + + /** + * If true, print the outputs of shell commands defined in event hooks to the console. + */ + // "printEventHooksOutputToConsole": true, + + /** + * If true, Rush will not allow node_modules in the repo folder or in parent folders. + */ + // "forbidPhantomResolvableNodeModulesFolders": true, + + /** + * (UNDER DEVELOPMENT) For certain installation problems involving peer dependencies, PNPM cannot + * correctly satisfy versioning requirements without installing duplicate copies of a package inside the + * node_modules folder. This poses a problem for "workspace:*" dependencies, as they are normally + * installed by making a symlink to the local project source folder. PNPM's "injected dependencies" + * feature provides a model for copying the local project folder into node_modules, however copying + * must occur AFTER the dependency project is built and BEFORE the consuming project starts to build. + * The "pnpm-sync" tool manages this operation; see its documentation for details. + * Enable this experiment if you want "rush" and "rushx" commands to resync injected dependencies + * by invoking "pnpm-sync" during the build. + */ + // "usePnpmSyncForInjectedDependencies": true, + + /** + * If set to true, Rush will generate a `project-impact-graph.yaml` file in the repository root during `rush update`. + */ + // "generateProjectImpactGraphDuringRushUpdate": true, + + /** + * If true, when running in watch mode, Rush will check for phase scripts named `_phase::ipc` and run them instead + * of `_phase:` if they exist. The created child process will be provided with an IPC channel and expected to persist + * across invocations. + */ + // "useIPCScriptsInWatchMode": true, + + /** + * (UNDER DEVELOPMENT) The Rush alerts feature provides a way to send announcements to engineers + * working in the monorepo, by printing directly in the user's shell window when they invoke Rush commands. + * This ensures that important notices will be seen by anyone doing active development, since people often + * ignore normal discussion group messages or don't know to subscribe. + */ + // "rushAlerts": true, + + + /** + * When using cobuilds, this experiment allows uncacheable operations to benefit from cobuild orchestration without using the build cache. + */ + // "allowCobuildWithoutCache": true, + + /** + * By default, rush perform a full scan of the entire repository. For example, Rush runs `git status` to check for local file changes. + * When this toggle is enabled, Rush will only scan specific paths, significantly speeding up Git operations. + */ + // "enableSubpathScan": true, + + /** + * Rush has a policy that normally requires Rush projects to specify `workspace:*` in package.json when depending + * on other projects in the workspace, unless they are explicitly declared as `decoupledLocalDependencies` + * in rush.json. Enabling this experiment will remove that requirement for dependencies belonging to a different + * subspace. This is useful for large product groups who work in separate subspaces and generally prefer to consume + * each other's packages via the NPM registry. + */ + // "exemptDecoupledDependenciesBetweenSubspaces": false +} diff --git a/foundations/core/common/config/rush/pnpm-config.json b/foundations/core/common/config/rush/pnpm-config.json new file mode 100644 index 0000000000..c6b1d837b5 --- /dev/null +++ b/foundations/core/common/config/rush/pnpm-config.json @@ -0,0 +1,329 @@ +/** + * This configuration file provides settings specific to the PNPM package manager. + * More documentation is available on the Rush website: https://rushjs.io + * + * Rush normally looks for this file in `common/config/rush/pnpm-config.json`. However, + * if `subspacesEnabled` is true in subspaces.json, then Rush will instead first look + * for `common/config/subspaces//pnpm-config.json`. (If the file exists in both places, + * then the file under `common/config/rush` is ignored.) + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + + /** + * If true, then `rush install` and `rush update` will use the PNPM workspaces feature + * to perform the install, instead of the old model where Rush generated the symlinks + * for each projects's node_modules folder. + * + * When using workspaces, Rush will generate a `common/temp/pnpm-workspace.yaml` file referencing + * all local projects to install. Rush will also generate a `.pnpmfile.cjs` shim which implements + * Rush-specific features such as preferred versions. The user's `common/config/rush/.pnpmfile.cjs` + * is invoked by the shim. + * + * This option is strongly recommended. The default value is false. + */ + "useWorkspaces": true, + + /** + * This setting determines how PNPM chooses version numbers during `rush update`. + * For example, suppose `lib-x@3.0.0` depends on `"lib-y": "^1.2.3"` whose latest major + * releases are `1.8.9` and `2.3.4`. The resolution mode `lowest-direct` might choose + * `lib-y@1.2.3`, wheres `highest` will choose 1.8.9, and `time-based` will pick the + * highest compatible version at the time when `lib-x@3.0.0` itself was published (ensuring + * that the version could have been tested by the maintainer of "lib-x"). For local workspace + * projects, `time-based` instead works like `lowest-direct`, avoiding upgrades unless + * they are explicitly requested. Although `time-based` is the most robust option, it may be + * slightly slower with registries such as npmjs.com that have not implemented an optimization. + * + * IMPORTANT: Be aware that PNPM 8.0.0 initially defaulted to `lowest-direct` instead of + * `highest`, but PNPM reverted this decision in 8.6.12 because it caused confusion for users. + * Rush version 5.106.0 and newer avoids this confusion by consistently defaulting to + * `highest` when `resolutionMode` is not explicitly set in pnpm-config.json or .npmrc, + * regardless of your PNPM version. + * + * PNPM documentation: https://pnpm.io/npmrc#resolution-mode + * + * Possible values are: `highest`, `time-based`, and `lowest-direct`. + * The default is `highest`. + */ + // "resolutionMode": "time-based", + + /** + * This setting determines whether PNPM will automatically install (non-optional) + * missing peer dependencies instead of reporting an error. Doing so conveniently + * avoids the need to specify peer versions in package.json, but in a large monorepo + * this often creates worse problems. The reason is that peer dependency behavior + * is inherently complicated, and it is easier to troubleshoot consequences of an explicit + * version than an invisible heuristic. The original NPM RFC discussion pointed out + * some other problems with this feature: https://github.com/npm/rfcs/pull/43 + + * IMPORTANT: Without Rush, the setting defaults to true for PNPM 8 and newer; however, + * as of Rush version 5.109.0 the default is always false unless `autoInstallPeers` + * is specified in pnpm-config.json or .npmrc, regardless of your PNPM version. + + * PNPM documentation: https://pnpm.io/npmrc#auto-install-peers + + * The default value is false. + */ + // "autoInstallPeers": false, + + /** + * If true, then Rush will add the `--strict-peer-dependencies` command-line parameter when + * invoking PNPM. This causes `rush update` to fail if there are unsatisfied peer dependencies, + * which is an invalid state that can cause build failures or incompatible dependency versions. + * (For historical reasons, JavaScript package managers generally do not treat this invalid + * state as an error.) + * + * PNPM documentation: https://pnpm.io/npmrc#strict-peer-dependencies + * + * The default value is false to avoid legacy compatibility issues. + * It is strongly recommended to set `strictPeerDependencies=true`. + */ + "strictPeerDependencies": true, + + /** + * Environment variables that will be provided to PNPM. + */ + // "environmentVariables": { + // "NODE_OPTIONS": { + // "value": "--max-old-space-size=4096", + // "override": false + // } + // }, + + /** + * Specifies the location of the PNPM store. There are two possible values: + * + * - `local` - use the `pnpm-store` folder in the current configured temp folder: + * `common/temp/pnpm-store` by default. + * - `global` - use PNPM's global store, which has the benefit of being shared + * across multiple repo folders, but the disadvantage of less isolation for builds + * (for example, bugs or incompatibilities when two repos use different releases of PNPM) + * + * In both cases, the store path can be overridden by the environment variable `RUSH_PNPM_STORE_PATH`. + * + * The default value is `local`. + */ + // "pnpmStore": "global", + + /** + * If true, then `rush install` will report an error if manual modifications + * were made to the PNPM shrinkwrap file without running `rush update` afterwards. + * + * This feature protects against accidental inconsistencies that may be introduced + * if the PNPM shrinkwrap file (`pnpm-lock.yaml`) is manually edited. When this + * feature is enabled, `rush update` will append a hash to the file as a YAML comment, + * and then `rush update` and `rush install` will validate the hash. Note that this + * does not prohibit manual modifications, but merely requires `rush update` be run + * afterwards, ensuring that PNPM can report or repair any potential inconsistencies. + * + * To temporarily disable this validation when invoking `rush install`, use the + * `--bypass-policy` command-line parameter. + * + * The default value is false. + */ + // "preventManualShrinkwrapChanges": true, + + /** + * When a project uses `workspace:` to depend on another Rush project, PNPM normally installs + * it by creating a symlink under `node_modules`. This generally works well, but in certain + * cases such as differing `peerDependencies` versions, symlinking may cause trouble + * such as incorrectly satisfied versions. For such cases, the dependency can be declared + * as "injected", causing PNPM to copy its built output into `node_modules` like a real + * install from a registry. Details here: https://rushjs.io/pages/advanced/injected_deps/ + * + * When using Rush subspaces, these sorts of versioning problems are much more likely if + * `workspace:` refers to a project from a different subspace. This is because the symlink + * would point to a separate `node_modules` tree installed by a different PNPM lockfile. + * A comprehensive solution is to enable `alwaysInjectDependenciesFromOtherSubspaces`, + * which automatically treats all projects from other subspaces as injected dependencies + * without having to manually configure them. + * + * NOTE: Use carefully -- excessive file copying can slow down the `rush install` and + * `pnpm-sync` operations if too many dependencies become injected. + * + * The default value is false. + */ + // "alwaysInjectDependenciesFromOtherSubspaces": false, + + /** + * Defines the policies to be checked for the `pnpm-lock.yaml` file. + */ + "pnpmLockfilePolicies": { + /** + * This policy will cause "rush update" to report an error if `pnpm-lock.yaml` contains + * any SHA1 integrity hashes. + * + * For each NPM dependency, `pnpm-lock.yaml` normally stores an `integrity` hash. Although + * its main purpose is to detect corrupted or truncated network requests, this hash can also + * serve as a security fingerprint to protect against attacks that would substitute a + * malicious tarball, for example if a misconfigured .npmrc caused a machine to accidentally + * download a matching package name+version from npmjs.com instead of the private NPM registry. + * NPM originally used a SHA1 hash; this was insecure because an attacker can too easily craft + * a tarball with a matching fingerprint. For this reason, NPM later deprecated SHA1 and + * instead adopted a cryptographically strong SHA512 hash. Nonetheless, SHA1 hashes can + * occasionally reappear during "rush update", for example due to missing metadata fallbacks + * (https://github.com/orgs/pnpm/discussions/6194) or an incompletely migrated private registry. + * The `disallowInsecureSha1` policy prevents this, avoiding potential security/compliance alerts. + */ + // "disallowInsecureSha1": { + // /** + // * Enables the "disallowInsecureSha1" policy. The default value is false. + // */ + // "enabled": true, + // + // /** + // * In rare cases, a private NPM registry may continue to serve SHA1 hashes for very old + // * package versions, perhaps due to a caching issue or database migration glitch. To avoid + // * having to disable the "disallowInsecureSha1" policy for the entire monorepo, the problematic + // * package versions can be individually ignored. The "exemptPackageVersions" key is the + // * package name, and the array value lists exact version numbers to be ignored. + // */ + // "exemptPackageVersions": { + // "example1": ["1.0.0"], + // "example2": ["2.0.0", "2.0.1"] + // } + // } + }, + + /** + * The "globalOverrides" setting provides a simple mechanism for overriding version selections + * for all dependencies of all projects in the monorepo workspace. The settings are copied + * into the `pnpm.overrides` field of the `common/temp/package.json` file that is generated + * by Rush during installation. + * + * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by + * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, + * and `globalOverrides` has lowest precedence. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmoverrides + */ + "globalOverrides": { + // "example1": "^1.0.0", + // "example2": "npm:@company/example2@^1.0.0" + }, + + /** + * The `globalPeerDependencyRules` setting provides various settings for suppressing validation errors + * that are reported during installation with `strictPeerDependencies=true`. The settings are copied + * into the `pnpm.peerDependencyRules` field of the `common/temp/package.json` file that is generated + * by Rush during installation. + * + * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by + * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, + * and `globalOverrides` has lowest precedence. + * + * https://pnpm.io/package_json#pnpmpeerdependencyrules + */ + "globalPeerDependencyRules": { + // "ignoreMissing": ["@eslint/*"], + // "allowedVersions": { "react": "17" }, + // "allowAny": ["@babel/*"] + }, + + /** + * The `globalPackageExtension` setting provides a way to patch arbitrary package.json fields + * for any PNPM dependency of the monorepo. The settings are copied into the `pnpm.packageExtensions` + * field of the `common/temp/package.json` file that is generated by Rush during installation. + * The `globalPackageExtension` setting has similar capabilities as `.pnpmfile.cjs` but without + * the downsides of an executable script (nondeterminism, unreliable caching, performance concerns). + * + * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by + * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, + * and `globalOverrides` has lowest precedence. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmpackageextensions + */ + "globalPackageExtensions": { + // "fork-ts-checker-webpack-plugin": { + // "dependencies": { + // "@babel/core": "1" + // }, + // "peerDependencies": { + // "eslint": ">= 6" + // }, + // "peerDependenciesMeta": { + // "eslint": { + // "optional": true + // } + // } + // } + }, + + /** + * The `globalNeverBuiltDependencies` setting suppresses the `preinstall`, `install`, and `postinstall` + * lifecycle events for the specified NPM dependencies. This is useful for scripts with poor practices + * such as downloading large binaries without retries or attempting to invoke OS tools such as + * a C++ compiler. (PNPM's terminology refers to these lifecycle events as "building" a package; + * it has nothing to do with build system operations such as `rush build` or `rushx build`.) + * The settings are copied into the `pnpm.neverBuiltDependencies` field of the `common/temp/package.json` + * file that is generated by Rush during installation. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmneverbuiltdependencies + */ + "globalNeverBuiltDependencies": [ + // "fsevents" + ], + + /** + * The `globalIgnoredOptionalDependencies` setting suppresses the installation of optional NPM + * dependencies specified in the list. This is useful when certain optional dependencies are + * not needed in your environment, such as platform-specific packages or dependencies that + * fail during installation but are not critical to your project. + * These settings are copied into the `pnpm.overrides` field of the `common/temp/package.json` + * file that is generated by Rush during installation, instructing PNPM to ignore the specified + * optional dependencies. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmignoredoptionaldependencies + */ + "globalIgnoredOptionalDependencies": [ + // "fsevents" + ], + + /** + * The `globalAllowedDeprecatedVersions` setting suppresses installation warnings for package + * versions that the NPM registry reports as being deprecated. This is useful if the + * deprecated package is an indirect dependency of an external package that has not released a fix. + * The settings are copied into the `pnpm.allowedDeprecatedVersions` field of the `common/temp/package.json` + * file that is generated by Rush during installation. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmalloweddeprecatedversions + * + * If you are working to eliminate a deprecated version, it's better to specify `allowedDeprecatedVersions` + * in the package.json file for individual Rush projects. + */ + "globalAllowedDeprecatedVersions": { + // "request": "*" + }, + + /** + * (THIS FIELD IS MACHINE GENERATED) The "globalPatchedDependencies" field is updated automatically + * by the `rush-pnpm patch-commit` command. It is a dictionary, where the key is an NPM package name + * and exact version, and the value is a relative path to the associated patch file. + * + * PNPM documentation: https://pnpm.io/package_json#pnpmpatcheddependencies + */ + "globalPatchedDependencies": {}, + + /** + * (USE AT YOUR OWN RISK) This is a free-form property bag that will be copied into + * the `common/temp/package.json` file that is generated by Rush during installation. + * This provides a way to experiment with new PNPM features. These settings will override + * any other Rush configuration associated with a given JSON field except for `.pnpmfile.cjs`. + * + * USAGE OF THIS SETTING IS NOT SUPPORTED BY THE RUSH MAINTAINERS AND MAY CAUSE RUSH + * TO MALFUNCTION. If you encounter a missing PNPM setting that you believe should + * be supported, please create a GitHub issue or PR. Note that Rush does not aim to + * support every possible PNPM setting, but rather to promote a battle-tested installation + * strategy that is known to provide a good experience for large teams with lots of projects. + */ + "unsupportedPackageJsonSettings": { + // "dependencies": { + // "not-a-good-practice": "*" + // }, + // "scripts": { + // "do-something": "echo Also not a good practice" + // }, + // "pnpm": { "futurePnpmFeature": true } + } +} diff --git a/foundations/core/common/config/rush/pnpm-lock.yaml b/foundations/core/common/config/rush/pnpm-lock.yaml new file mode 100644 index 0000000000..16d79d3fc1 --- /dev/null +++ b/foundations/core/common/config/rush/pnpm-lock.yaml @@ -0,0 +1,10389 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +pnpmfileChecksum: sha256-La8sfCMI7irxJPTW6u4mJb4vNiyLrXeEbKaXv5G3dL0= + +importers: + + .: {} + + ../../packages/account-client: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + esbuild: + specifier: ^0.25.9 + version: 0.25.11 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/analytics: + dependencies: + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/analytics-service: + dependencies: + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../analytics + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/measurements-otlp': + specifier: workspace:^0.7.17 + version: link:../measurements-otlp + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + winston: + specifier: ^3.11.0 + version: 3.18.3 + winston-daily-rotate-file: + specifier: ^5.0.0 + version: 5.0.0(winston@3.18.3) + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/api-client: + dependencies: + '@hcengineering/account-client': + specifier: workspace:^0.7.19 + version: link:../account-client + '@hcengineering/client': + specifier: workspace:^0.7.17 + version: link:../client + '@hcengineering/client-resources': + specifier: workspace:^0.7.17 + version: link:../client-resources + '@hcengineering/collaborator-client': + specifier: workspace:^0.7.17 + version: link:../collaborator-client + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + '@hcengineering/text': + specifier: workspace:^0.7.18 + version: link:../text + '@hcengineering/text-markdown': + specifier: workspace:^0.7.19 + version: link:../text-markdown + snappyjs: + specifier: ^0.7.0 + version: 0.7.0 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@types/snappyjs': + specifier: ^0.7.1 + version: 0.7.1 + '@types/ws': + specifier: ^8.5.12 + version: 8.18.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + ts-node: + specifier: ^10.8.0 + version: 10.9.2(@types/node@22.18.10)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + optionalDependencies: + ws: + specifier: ^8.18.2 + version: 8.18.3 + + ../../packages/client: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/client-resources: + dependencies: + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../analytics + '@hcengineering/client': + specifier: workspace:^0.7.17 + version: link:../client + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + '@hcengineering/rpc': + specifier: workspace:^0.7.17 + version: link:../rpc + snappyjs: + specifier: ^0.7.0 + version: 0.7.0 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@types/snappyjs': + specifier: ^0.7.1 + version: 0.7.1 + '@types/ws': + specifier: ^8.5.12 + version: 8.18.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + ws: + specifier: ^8.18.2 + version: 8.18.3 + + ../../packages/collaborator-client: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + esbuild: + specifier: ^0.25.9 + version: 0.25.11 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/core: + dependencies: + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../analytics + '@hcengineering/measurements': + specifier: workspace:^0.7.18 + version: link:../measurements + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + fast-equals: + specifier: ^5.2.2 + version: 5.3.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/hulylake-client: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/retry': + specifier: workspace:^0.7.17 + version: link:../retry + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + esbuild: + specifier: ^0.25.9 + version: 0.25.11 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/measurements: + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-node: + specifier: ^11.1.0 + version: 11.1.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + + ../../packages/measurements-otlp: + dependencies: + '@hcengineering/measurements': + specifier: workspace:^0.7.18 + version: link:../measurements + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/api-logs': + specifier: ^0.203.0 + version: 0.203.0 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.62.0 + version: 0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0)) + '@opentelemetry/core': + specifier: ^2.0.1 + version: 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/id-generator-aws-xray': + specifier: ^2.0.0 + version: 2.0.3(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^2.0.1 + version: 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^2.0.1 + version: 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^2.0.1 + version: 2.1.0(@opentelemetry/api@1.9.0) + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-node: + specifier: ^11.1.0 + version: 11.1.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + + ../../packages/model: + dependencies: + '@hcengineering/account-client': + specifier: workspace:^0.7.19 + version: link:../account-client + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../analytics + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + '@hcengineering/rank': + specifier: workspace:^0.7.17 + version: link:../rank + '@hcengineering/storage': + specifier: workspace:^0.7.17 + version: link:../storage + fast-equals: + specifier: ^5.2.2 + version: 5.3.2 + toposort: + specifier: ^2.0.2 + version: 2.0.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@types/toposort': + specifier: ^2.0.3 + version: 2.0.7 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/platform: + dependencies: + intl-messageformat: + specifier: ^10.7.14 + version: 10.7.18 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/postgres-base: + dependencies: + postgres: + specifier: ^3.4.7 + version: 3.4.7 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-node: + specifier: ^11.1.0 + version: 11.1.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + + ../../packages/query: + dependencies: + '@hcengineering/analytics': + specifier: workspace:^0.7.17 + version: link:../analytics + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + fast-equals: + specifier: ^5.2.2 + version: 5.3.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/rank: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + lexorank: + specifier: ~1.0.4 + version: 1.0.5 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/retry: + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/rpc: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + msgpackr: + specifier: ^1.11.2 + version: 1.11.5 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/storage: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + fast-equals: + specifier: ^5.2.2 + version: 5.3.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/storage-client: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + esbuild: + specifier: ^0.25.9 + version: 0.25.11 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/text: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/text-core': + specifier: workspace:^0.7.18 + version: link:../text-core + '@tiptap/core': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/extension-blockquote': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-bold': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-bullet-list': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-code': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-code-block': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-document': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-dropcursor': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-gapcursor': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-hard-break': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-heading': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-highlight': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-history': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-horizontal-rule': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-italic': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-link': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-list-item': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-mention': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(@tiptap/suggestion@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)) + '@tiptap/extension-ordered-list': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-paragraph': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-strike': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-table': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-table-cell': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-table-header': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-table-row': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-task-item': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-task-list': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-text': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-text-align': + specifier: ~2.11.0 + version: 2.11.9(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-text-style': + specifier: ~2.11.0 + version: 2.11.9(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-typography': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-underline': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/html': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/pm': + specifier: ^2.11.7 + version: 2.26.3 + '@tiptap/starter-kit': + specifier: ^2.11.7 + version: 2.26.3 + '@tiptap/suggestion': + specifier: ^2.11.7 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + fast-equals: + specifier: ^5.2.2 + version: 5.3.2 + prosemirror-codemark: + specifier: ^0.4.2 + version: 0.4.2(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3) + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/markdown-it': + specifier: ~13.0.0 + version: 13.0.9 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/text-core: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + fast-equals: + specifier: ^5.2.2 + version: 5.3.2 + hash-it: + specifier: ^6.0.0 + version: 6.0.0 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/markdown-it': + specifier: ~13.0.0 + version: 13.0.9 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest-environment-jsdom: + specifier: ^29.7.0 + version: 29.7.0 + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/text-html: + dependencies: + '@hcengineering/text-core': + specifier: workspace:^0.7.18 + version: link:../text-core + htmlparser2: + specifier: ^9.0.0 + version: 9.1.0 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/text-markdown: + dependencies: + '@hcengineering/text-core': + specifier: workspace:^0.7.18 + version: link:../text-core + '@hcengineering/text-html': + specifier: workspace:^0.7.18 + version: link:../text-html + fast-equals: + specifier: ^5.2.2 + version: 5.3.2 + markdown-it: + specifier: ^14.0.0 + version: 14.1.0 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/markdown-it': + specifier: ~13.0.0 + version: 13.0.9 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/text-ydoc: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/text': + specifier: workspace:^0.7.18 + version: link:../text + '@hcengineering/text-core': + specifier: workspace:^0.7.18 + version: link:../text-core + y-protocols: + specifier: ^1.0.6 + version: 1.0.6(yjs@13.6.27) + yjs: + specifier: ^13.6.27 + version: 13.6.27 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + fast-equals: + specifier: ^5.2.2 + version: 5.3.2 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest-environment-jsdom: + specifier: ^29.7.0 + version: 29.7.0 + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + y-prosemirror: + specifier: ^1.3.7 + version: 1.3.7(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + + ../../packages/token: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.22 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.18 + version: link:../platform + jwt-simple: + specifier: ^0.5.6 + version: 0.5.6 + uuid: + specifier: ^8.3.2 + version: 8.3.2 + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.15.29 + version: 22.18.10 + '@types/uuid': + specifier: ^8.3.1 + version: 8.3.4 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: + specifier: ^3.1.0 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../scripts: + devDependencies: + '@hcengineering/platform-rig': + specifier: ^0.7.19 + version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + + '@esbuild/aix-ppc64@0.25.11': + resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.11': + resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.11': + resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.11': + resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.11': + resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.11': + resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.11': + resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.11': + resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.11': + resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.11': + resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.11': + resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.11': + resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.11': + resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.11': + resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.11': + resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.11': + resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.11': + resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.11': + resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.11': + resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.11': + resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.11': + resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.11': + resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.11': + resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.11': + resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.11': + resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.11': + resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@formatjs/ecma402-abstract@2.3.6': + resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.4': + resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} + + '@formatjs/icu-skeleton-parser@1.8.16': + resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} + + '@formatjs/intl-localematcher@0.6.2': + resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + + '@grpc/grpc-js@1.14.0': + resolution: {integrity: sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + + '@hcengineering/platform-rig@0.7.19': + resolution: {integrity: sha512-3Fi5nU+nEPjzzcm3FIfEZU+ZgdkiSfL46ojbj42jdXnWMkxew2aZZVtfmIpPnMUP9AMZlj2/EA+yOcMzfY/VmA==} + hasBin: true + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@opentelemetry/api-logs@0.203.0': + resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/auto-instrumentations-node@0.62.2': + resolution: {integrity: sha512-Ipe6X7ddrCiRsuewyTU83IvKiSFT4piqmv9z8Ovg1E7v98pdTj1pUE6sDrHV50zl7/ypd+cONBgt+EYSZu4u9Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.4.1 + '@opentelemetry/core': ^2.0.0 + + '@opentelemetry/context-async-hooks@2.0.1': + resolution: {integrity: sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/context-async-hooks@2.1.0': + resolution: {integrity: sha512-zOyetmZppnwTyPrt4S7jMfXiSX9yyfF0hxlA8B5oo2TtKl+/RGCy7fi4DrBfIf3lCPrkKsRBWZZD7RFojK7FDg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.0.1': + resolution: {integrity: sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.1.0': + resolution: {integrity: sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.203.0': + resolution: {integrity: sha512-g/2Y2noc/l96zmM+g0LdeuyYKINyBwN6FJySoU15LHPLcMN/1a0wNk2SegwKcxrRdE7Xsm7fkIR5n6XFe3QpPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.203.0': + resolution: {integrity: sha512-s0hys1ljqlMTbXx2XiplmMJg9wG570Z5lH7wMvrZX6lcODI56sG4HL03jklF63tBeyNwK2RV1/ntXGo3HgG4Qw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.203.0': + resolution: {integrity: sha512-nl/7S91MXn5R1aIzoWtMKGvqxgJgepB/sH9qW0rZvZtabnsjbf8OQ1uSx3yogtvLr0GzwD596nQKz2fV7q2RBw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0': + resolution: {integrity: sha512-FCCj9nVZpumPQSEI57jRAA89hQQgONuoC35Lt+rayWY/mzCAc6BQT7RFyFaZKJ2B7IQ8kYjOCPsF/HGFWjdQkQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.203.0': + resolution: {integrity: sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.203.0': + resolution: {integrity: sha512-OZnhyd9npU7QbyuHXFEPVm3LnjZYifuKpT3kTnF84mXeEQ84pJJZgyLBpU4FSkSwUkt/zbMyNAI7y5+jYTWGIg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.203.0': + resolution: {integrity: sha512-2jLuNuw5m4sUj/SncDf/mFPabUxMZmmYetx5RKIMIQyPnl6G6ooFzfeE8aXNRf8YD1ZXNlCnRPcISxjveGJHNg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.203.0': + resolution: {integrity: sha512-322coOTf81bm6cAA8+ML6A+m4r2xTCdmAZzGNTboPXRzhwPt4JEmovsFAs+grpdarObd68msOJ9FfH3jxM6wqA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.203.0': + resolution: {integrity: sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.203.0': + resolution: {integrity: sha512-1xwNTJ86L0aJmWRwENCJlH4LULMG2sOXWIVw+Szta4fkqKVY50Eo4HoVKKq6U9QEytrWCr8+zjw0q/ZOeXpcAQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@2.0.1': + resolution: {integrity: sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/id-generator-aws-xray@2.0.3': + resolution: {integrity: sha512-IoJiHlNAYn/XetYUwWR2HvAQxzoh7Ef/V2toKjnRz26RckfywhJAGlzKMWc0D7XyKMbBxGGg8K9IcDVu1JuJXQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-amqplib@0.50.0': + resolution: {integrity: sha512-kwNs/itehHG/qaQBcVrLNcvXVPW0I4FCOVtw3LHMLdYIqD7GJ6Yv2nX+a4YHjzbzIeRYj8iyMp0Bl7tlkidq5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-lambda@0.54.1': + resolution: {integrity: sha512-qm8pGSAM1mXk7unbrGktWWGJc6IFI58ZsaHJ+i420Fp5VO3Vf7GglIgaXTS8CKBrVB4LHFj3NvzJg31PtsAQcA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-sdk@0.58.0': + resolution: {integrity: sha512-9vFH7gU686dsAeLMCkqUj9y0MQZ1xrTtStSpNV2UaGWtDnRjJrAdJLu9Y545oKEaDTeVaob4UflyZvvpZnw3Xw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-bunyan@0.49.0': + resolution: {integrity: sha512-ky5Am1y6s3Ex/3RygHxB/ZXNG07zPfg9Z6Ora+vfeKcr/+I6CJbWXWhSBJor3gFgKN3RvC11UWVURnmDpBS6Pg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cassandra-driver@0.49.0': + resolution: {integrity: sha512-BNIvqldmLkeikfI5w5Rlm9vG5NnQexfPoxOgEMzfDVOEF+vS6351I6DzWLLgWWR9CNF/jQJJi/lr6am2DLp0Rw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.47.0': + resolution: {integrity: sha512-pjenvjR6+PMRb6/4X85L4OtkQCootgb/Jzh/l/Utu3SJHBid1F+gk9sTGU2FWuhhEfV6P7MZ7BmCdHXQjgJ42g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cucumber@0.19.0': + resolution: {integrity: sha512-99ms8kQWRuPt5lkDqbJJzD+7Tq5TMUlBZki4SA2h6CgK4ncX+tyep9XFY1e+XTBLJIWmuFMGbWqBLJ4fSKIQNQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-dataloader@0.21.1': + resolution: {integrity: sha512-hNAm/bwGawLM8VDjKR0ZUDJ/D/qKR3s6lA5NV+btNaPVm2acqhPcT47l2uCVi+70lng2mywfQncor9v8/ykuyw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dns@0.47.0': + resolution: {integrity: sha512-775fOnewWkTF4iXMGKgwvOGqEmPrU1PZpXjjqvTrEErYBJe7Fz1WlEeUStHepyKOdld7Ghv7TOF/kE3QDctvrg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.52.0': + resolution: {integrity: sha512-W7pizN0Wh1/cbNhhTf7C62NpyYw7VfCFTYg0DYieSTrtPBT1vmoSZei19wfKLnrMsz3sHayCg0HxCVL2c+cz5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fastify@0.48.0': + resolution: {integrity: sha512-3zQlE/DoVfVH6/ycuTv7vtR/xib6WOa0aLFfslYcvE62z0htRu/ot8PV/zmMZfnzpTQj8S/4ULv36R6UIbpJIg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.23.0': + resolution: {integrity: sha512-Puan+QopWHA/KNYvDfOZN6M/JtF6buXEyD934vrb8WhsX1/FuM7OtoMlQyIqAadnE8FqqDL4KDPiEfCQH6pQcQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.47.0': + resolution: {integrity: sha512-UfHqf3zYK+CwDwEtTjaD12uUqGGTswZ7ofLBEdQ4sEJp9GHSSJMQ2hT3pgBxyKADzUdoxQAv/7NqvL42ZI+Qbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.51.0': + resolution: {integrity: sha512-LchkOu9X5DrXAnPI1+Z06h/EH/zC7D6sA86hhPrk3evLlsJTz0grPrkL/yUJM9Ty0CL/y2HSvmWQCjbJEz/ADg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-grpc@0.203.0': + resolution: {integrity: sha512-Qmjx2iwccHYRLoE4RFS46CvQE9JG9Pfeae4EPaNZjvIuJxb/pZa2R9VWzRlTehqQWpAvto/dGhtkw8Tv+o0LTg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.50.0': + resolution: {integrity: sha512-5xGusXOFQXKacrZmDbpHQzqYD1gIkrMWuwvlrEPkYOsjUqGUjl1HbxCsn5Y9bUXOCgP1Lj6A4PcKt1UiJ2MujA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.203.0': + resolution: {integrity: sha512-y3uQAcCOAwnO6vEuNVocmpVzG3PER6/YZqbPbbffDdJ9te5NkHEkfSMNzlC3+v7KlE+WinPGc3N7MR30G1HY2g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.51.0': + resolution: {integrity: sha512-9IUws0XWCb80NovS+17eONXsw1ZJbHwYYMXiwsfR9TSurkLV5UNbRSKb9URHO+K+pIJILy9wCxvyiOneMr91Ig==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.13.0': + resolution: {integrity: sha512-FPQyJsREOaGH64hcxlzTsIEQC4DYANgTwHjiB7z9lldmvua1LRMVn3/FfBlzXoqF179B0VGYviz6rn75E9wsDw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.48.0': + resolution: {integrity: sha512-V5wuaBPv/lwGxuHjC6Na2JFRjtPgstw19jTFl1B1b6zvaX8zVDYUDaR5hL7glnQtUSCMktPttQsgK4dhXpddcA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.51.0': + resolution: {integrity: sha512-XNLWeMTMG1/EkQBbgPYzCeBD0cwOrfnn8ao4hWgLv0fNCFQu1kCsJYygz2cvKuCs340RlnG4i321hX7R8gj3Rg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.48.0': + resolution: {integrity: sha512-KUW29wfMlTPX1wFz+NNrmE7IzN7NWZDrmFWHM/VJcmFEuQGnnBuTIdsP55CnBDxKgQ/qqYFp4udQFNtjeFosPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-memcached@0.47.0': + resolution: {integrity: sha512-vXDs/l4hlWy1IepPG1S6aYiIZn+tZDI24kAzwKKJmR2QEJRL84PojmALAEJGazIOLl/VdcCPZdMb0U2K0VzojA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.56.0': + resolution: {integrity: sha512-YG5IXUUmxX3Md2buVMvxm9NWlKADrnavI36hbJsihqqvBGsWnIfguf0rUP5Srr0pfPqhQjUP+agLMsvu0GmUpA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.50.0': + resolution: {integrity: sha512-Am8pk1Ct951r4qCiqkBcGmPIgGhoDiFcRtqPSLbJrUZqEPUsigjtMjoWDRLG1Ki1NHgOF7D0H7d+suWz1AAizw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.50.0': + resolution: {integrity: sha512-PoOMpmq73rOIE3nlTNLf3B1SyNYGsp7QXHYKmeTZZnJ2Ou7/fdURuOhWOI0e6QZ5gSem18IR1sJi6GOULBQJ9g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.49.0': + resolution: {integrity: sha512-QU9IUNqNsrlfE3dJkZnFHqLjlndiU39ll/YAAEvWE40sGOCi9AtOF6rmEGzJ1IswoZ3oyePV7q2MP8SrhJfVAA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-nestjs-core@0.49.0': + resolution: {integrity: sha512-1R/JFwdmZIk3T/cPOCkVvFQeKYzbbUvDxVH3ShXamUwBlGkdEu5QJitlRMyVNZaHkKZKWgYrBarGQsqcboYgaw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-net@0.47.0': + resolution: {integrity: sha512-csoJ++Njpf7C09JH+0HNGenuNbDZBqO1rFhMRo6s0rAmJwNh9zY3M/urzptmKlqbKnf4eH0s+CKHy/+M8fbFsQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-oracledb@0.29.0': + resolution: {integrity: sha512-2aHLiJdkyiUbooIUm7FaZf+O4jyqEl+RfFpgud1dxT87QeeYM216wi+xaMNzsb5yKtRBqbA3qeHBCyenYrOZwA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.56.1': + resolution: {integrity: sha512-0/PiHDPVaLdcXNw6Gqb3JBdMxComMEwh444X8glwiynJKJHRTR49+l2cqJfoOVzB8Sl1XRl3Yaqw6aDi3s8e9w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pino@0.50.1': + resolution: {integrity: sha512-pBbvuWiHA9iAumAuQ0SKYOXK7NRlbnVTf/qBV0nMdRnxBPrc/GZTbh0f7Y59gZfYsbCLhXLL1oRTEnS+PwS3CA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.52.0': + resolution: {integrity: sha512-R8Y7cCZlJ2Vl31S2i7bl5SqyC/aul54ski4wCFip/Tp9WGtLK1xVATi2rwy2wkc8ZCtjdEe9eEVR+QFG6gGZxg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-restify@0.49.0': + resolution: {integrity: sha512-tsGZZhS4mVZH7omYxw5jpsrD3LhWizqWc0PYtAnzpFUvL5ZINHE+cm57bssTQ2AK/GtZMxu9LktwCvIIf3dSmw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-router@0.48.0': + resolution: {integrity: sha512-Wixrc8CchuJojXpaS/dCQjFOMc+3OEil1H21G+WLYQb8PcKt5kzW9zDBT19nyjjQOx/D/uHPfgbrT+Dc7cfJ9w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-runtime-node@0.17.1': + resolution: {integrity: sha512-c1FlAk+bB2uF9a8YneGmNPTl7c/xVaan4mmWvbkWcOmH/ipKqR1LaKUlz/BMzLrJLjho1EJlG2NrS2w2Arg+nw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-socket.io@0.50.0': + resolution: {integrity: sha512-6JN6lnKN9ZuZtZdMQIR+no1qHzQvXSZUsNe3sSWMgqmNRyEXuDUWBIyKKeG0oHRHtR4xE4QhJyD4D5kKRPWZFA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.22.0': + resolution: {integrity: sha512-XrrNSUCyEjH1ax9t+Uo6lv0S2FCCykcF7hSxBMxKf7Xn0bPRxD3KyFUZy25aQXzbbbUHhtdxj3r2h88SfEM3aA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.14.0': + resolution: {integrity: sha512-2HN+7ztxAReXuxzrtA3WboAKlfP5OsPA57KQn2AdYZbJ3zeRPcLXyW4uO/jpLE6PLm0QRtmeGCmfYpqRlwgSwg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation-winston@0.48.1': + resolution: {integrity: sha512-XyOuVwdziirHHYlsw+BWrvdI/ymjwnexupKA787zQQ+D5upaE/tseZxjfQa7+t4+FdVLxHICaMTmkSD4yZHpzQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.203.0': + resolution: {integrity: sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.203.0': + resolution: {integrity: sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.203.0': + resolution: {integrity: sha512-te0Ze1ueJF+N/UOFl5jElJW4U0pZXQ8QklgSfJ2linHN0JJsuaHG8IabEUi2iqxY8ZBDlSiz1Trfv5JcjWWWwQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.203.0': + resolution: {integrity: sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@2.0.1': + resolution: {integrity: sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@2.0.1': + resolution: {integrity: sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/redis-common@0.38.2': + resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} + engines: {node: ^18.19.0 || >=20.6.0} + + '@opentelemetry/resource-detector-alibaba-cloud@0.31.9': + resolution: {integrity: sha512-V+HbpICyzmJoQHYpiN0xRlj7QqeR9pPo+JZiZztV77L2MdlUCa/Cq7h0gdFNIKc0P9u9rYYYW21oaqdhhC5LZg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-aws@2.6.0': + resolution: {integrity: sha512-atZ9/HNXh9ZJuMZUH2TPl89imFZBaoiU0Mksa70ysVhYRzhk3hfJyiu+eETjZ7NhGjBPrd3sfVYEq/St/7+o3g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-azure@0.10.0': + resolution: {integrity: sha512-5cNAiyPBg53Uxe/CW7hsCq8HiKNAUGH+gi65TtgpzSR9bhJG4AEbuZhbJDFwe97tn2ifAD1JTkbc/OFuaaFWbA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-container@0.7.9': + resolution: {integrity: sha512-BiS14kCylLzh/mayN/sjnOdhnpfgiekaEsIzaL29MErfQR0mFCZjAE2uu8jMjShva9bSDFs65ouuAFft+vBthg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-gcp@0.37.0': + resolution: {integrity: sha512-LGpJBECIMsVKhiulb4nxUw++m1oF4EiDDPmFGW2aqYaAF0oUvJNv8Z/55CAzcZ7SxvlTgUwzewXDBsuCup7iqw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resources@2.0.1': + resolution: {integrity: sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.1.0': + resolution: {integrity: sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.203.0': + resolution: {integrity: sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.0.1': + resolution: {integrity: sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.1.0': + resolution: {integrity: sha512-J9QX459mzqHLL9Y6FZ4wQPRZG4TOpMCyPOh6mkr/humxE1W2S3Bvf4i75yiMW9uyed2Kf5rxmLhTm/UK8vNkAw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.203.0': + resolution: {integrity: sha512-zRMvrZGhGVMvAbbjiNQW3eKzW/073dlrSiAKPVWmkoQzah9wfynpVPeL55f9fVIm0GaBxTLcPeukWGy0/Wj7KQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.0.1': + resolution: {integrity: sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.1.0': + resolution: {integrity: sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.0.1': + resolution: {integrity: sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.1.0': + resolution: {integrity: sha512-SvVlBFc/jI96u/mmlKm86n9BbTCbQ35nsPoOohqJX6DXH92K0kTe73zGY5r8xoI1QkjR9PizszVJLzMC966y9Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.37.0': + resolution: {integrity: sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.41.2': + resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + + '@tiptap/core@2.26.3': + resolution: {integrity: sha512-TaOJzu2v5ufsOx+yu94NqXE504zmupVdFCxH1g3hk5fzZ3gT57Lh9R/27OjwM4e6o+Z3DXDl8yfFMHIcR3zUkg==} + peerDependencies: + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-blockquote@2.26.3': + resolution: {integrity: sha512-brz8+wh03TuMevNUztTSC9BzZEsLCNakPJCCicD8FRpBJoLj4benT6T3GYVdMhkk4BmhpruSFZB0FPY+rxCVlA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bold@2.26.3': + resolution: {integrity: sha512-ssXKQxSwQ+Webv65emK/A1d13iTvnfbw8I2wlzuxsrMChyb4wH2HyqI5N4g0FpLqCpkXFumforoY+0XKktve+w==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bullet-list@2.26.3': + resolution: {integrity: sha512-pfBMOup1JbXgf2aVTtG1A5t7qFZJrpD+wNPuypjF2YWmCl/pAlwbPFz9hNuWyZq14+QoQg5tML1/G1M7cgrrtw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-code-block@2.26.3': + resolution: {integrity: sha512-3DbzKRfMqw9EGS7mGkpyopbRWTO+qpV52Mby4Ll2+OfhvGnHzSN4Q7xOsp+VeZr14GMEmua5Oq2e/gRypqXatQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-code@2.26.3': + resolution: {integrity: sha512-bAkUNzV+tA1J1RYbtbAGTFqkRw9+yRpAd+d3S9jy/dAD+uOe1ZD1EIngyEf2GTonnoy4bpDYtytbCjUt9PozoA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-document@2.26.3': + resolution: {integrity: sha512-gcJg4Otchilr4eSUwhPNwbhPUkEYvXhkUZ/1MAhVGD40Ovq2P8ZWkJipA3tKOCJinL5MJK59ccZBstnKSTw+JA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-dropcursor@2.26.3': + resolution: {integrity: sha512-54rgDTmRStVmXZR7KdCvSOCAbumh5luXgticUkRM8OM8PBe1c0T9X8jfV7+XEFGugRVl8mtCZZpgUt5vhuxHog==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-gapcursor@2.26.3': + resolution: {integrity: sha512-ZDNSkpz7ik2PJOjrys27rwko5Ufe6GtLjaAxjvkWmyzcgAOTadDeth9NaRdBVMDGgSLBKbXihYZZXLkiAP9RLA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-hard-break@2.26.3': + resolution: {integrity: sha512-KJWUi+2KOZejVRb2KI0NM3LgCpNimxcunbOCKsZKygV/UByzhUl7UaCAIa+ySMM+kbu/Ec3hkTzafGfaU9ZkLg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-heading@2.26.3': + resolution: {integrity: sha512-bp7YildFOustuGJGl8TInG26h7xbcpBKskm49TjwyBjUqRHPGH4V11554afStAr+bsTlPN4TDXt7extvq3UYLA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-highlight@2.26.3': + resolution: {integrity: sha512-cW5V+9es7UPLUQgU4I9gqj9w4G4PgWwJMxB107ChCAsFEb2IvC2fDcwRCHY+xiLJGPq0xZag/kvtx0uZkovITw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-history@2.26.3': + resolution: {integrity: sha512-Qg4+WWf/hDgiBspxLbrhrIFUy7lzi2eBKPSoF/haEYFw/t/FeN60NXYYYtpLimUNpUzyJSOSIwsngFcVJO5X+g==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-horizontal-rule@2.26.3': + resolution: {integrity: sha512-NhlJEDj0b/P1Rj4UOMgt4CjS4IXEhXQFsdiXmsYZxchfr4J72HrsOfZs4vAqIQbkrLgUlYEr/DGMNWzME78FrA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-italic@2.26.3': + resolution: {integrity: sha512-DJX31JQsyerqoNM+hAtbjHoJ42W/EpnMMCtQr/gRS8ssEdrVtcDDhSO2tkaP6dNjhG8zH2hKYsXpLCCFdDgvwg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-link@2.26.3': + resolution: {integrity: sha512-cNYqAeiaG/65ctVEUOHt1MQnTF1JcdZqBkN9pLf3grzcmkmdr3w1/JbKOphZc84vOB2rxuhGZx9NFV2lrC5Qwg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-list-item@2.26.3': + resolution: {integrity: sha512-9qU0SoC+tDSKYhfdWFS3dkioEk3ml1ycBeRmOxh7h+w0ezmTomiT5yvc9t3KM30ps8n1p78sIPo19GF65u1dFQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-mention@2.26.3': + resolution: {integrity: sha512-VZi4Qrg8Q8Lg2v/YfuYuTjmyEtpO3cuy2racIIyXw11LhQSQ5rflHK+0mWKo00bhx2mPxxod4DYIR31DqSfvPA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + '@tiptap/suggestion': ^2.7.0 + + '@tiptap/extension-ordered-list@2.26.3': + resolution: {integrity: sha512-x6G0qA7dAvSq+kphA7P64m+ScoVEAW8s9pl7o3jIJzcIW/LrbL1xkyOjbgCvGEvwyQVsgyqtLQDQ2oeloosDBw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-paragraph@2.26.3': + resolution: {integrity: sha512-eBC5UsaTJRUMhePtK1dcCAfes0CpqqFiewpIM0lWk4XMtpG2aoczVVVkImybbFKfqsvEEo3vgHJ2YiE5YZFCSg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-strike@2.26.3': + resolution: {integrity: sha512-Po3al5hP0IwvHHIHYy3DbUvCD/kbYTsi3sWTjPAB9QgqaoJGl+jyhIyha8FsR+U3MCIIJIekMktI5o1+ySMGpg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-table-cell@2.26.3': + resolution: {integrity: sha512-m/uZSeXuRAJaLedq0MOu9ZGibh4kkovXX0i5Oj6K9lT+TtBLQBNCSABQeOCe2FFPXhpWRpnhZrqhJgdo/n9BSg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-table-header@2.26.3': + resolution: {integrity: sha512-8+P3j5kNE04zbqGqwEFKJ82ECBoRfx6PaPkoNlftBkRnAQIWajdPcLxLpBPak1tHw9sEtnZg38aBFz7kQLcklg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-table-row@2.26.3': + resolution: {integrity: sha512-eJC9/iWAEi/7nxL7I/3d+pUOarp8ns8cQoN0xbZnQ40irSzNXY6vpCskVm+1IulhgCbGyOtjiS8z06wBck1X9Q==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-table@2.26.3': + resolution: {integrity: sha512-Ycvem797qPazgY+9OUXL8EXS2XCnb45y1IPi6gOhP1DStaGSVMhNBWvuLipSbu8UBpB+yIJ4/sqIvlLqsZQceQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-task-item@2.26.3': + resolution: {integrity: sha512-Pe2B/57qNPW1XlH44qdwYeKyBGYKw6TiPXg1pvdhu9zyUglu4J4gbvvn224Z5D9IHlSbPSxr4ss6DNm5S64I/w==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-task-list@2.26.3': + resolution: {integrity: sha512-VSQlU9m3uQ9ReJ4CYM6+IOepLdvNHBA6P/ti/bArb0vUseDfkx0KsQINTmxnXLPF+NLQnk1uXVk5/TJg1+OdMg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text-align@2.11.9': + resolution: {integrity: sha512-MaCL83TPiDX20vdKSe+QwghBcJ5HDtrK6btGqgW4pugae0Pi71oe2lqkeM+eJBsiXN4WG+x9XMwBRd9vUnLF0g==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text-style@2.11.9': + resolution: {integrity: sha512-lB3uJBRiRYTCxtNeEF70IiwzGFfAU69xvsHpQowJU357lZsTj/QPRQt8Blu48qIoUAJGU00vGRTLae6X9myMMw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text-style@2.26.3': + resolution: {integrity: sha512-B+t6k41xtmlIxyi0r+g8MAShGMCK6kmz8EdxoLAUVrlCxYWVk6qvzoojZbjQKlb2sE+idIo4X5yCcKpdkxFe0w==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text@2.26.3': + resolution: {integrity: sha512-sGRbX96ss4jQeKw9d0iphuAWja8Dv4w4ryTDKfxD7Lizx3UaIxQB/y+Wna89tM3kfbi/qJcrD3AF7NJgfc/tEA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-typography@2.26.3': + resolution: {integrity: sha512-GgWICb2FFCFzd2JJ+ECrFubcEwEtTsFcKR71jvnvVw22pgKHIp0C7PTmpzJTBVynoha+MgIweWMzC2ZtvUNNqQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-underline@2.26.3': + resolution: {integrity: sha512-FXQUiHjvKDIpU9Bg+fV7UqQnEjhGvVQUnrf9VOI1/9hm+GWWxAfV2asRiZgV6jeBvNJWYzUGzvQUN6vFzmVbdw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/html@2.26.3': + resolution: {integrity: sha512-4xC91c2/qwWoQULh/JFFoRdI0wJyA/8AhYpgI549u+F4mzZ9HvEI/ztx0dMFvft7vUC/1iszVkLajOOBA33xPg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/pm@2.26.3': + resolution: {integrity: sha512-8gUmdxWlUevmgq2mNvGxvf2CpDW097tVKECMWKEn8sf846kXv3CoqaGRhI3db4kfR+09uWZeRM7rtrjRBmUThg==} + + '@tiptap/starter-kit@2.26.3': + resolution: {integrity: sha512-hznj/j+mFIuKfNB0ToaZVcVjdtpSOHoBoX3ocSz9BaYCtK+nX1c0gTlfbJ1BcpYUZNtqG+tpUeIfvXifRkq/OQ==} + + '@tiptap/suggestion@2.26.3': + resolution: {integrity: sha512-kcyiyKEEDnqFImGQQEEuRa6N/N+/vU/OrI99wRfJnDnN8c3dP6UHJ4wr2qX6bUpx3Z0QTu6GGCpMpaqwtHTtJg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/aws-lambda@8.10.152': + resolution: {integrity: sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/bunyan@1.8.11': + resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/jsdom@20.0.1': + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/linkify-it@3.0.5': + resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@13.0.9': + resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@1.0.5': + resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/memcached@2.2.10': + resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} + + '@types/mysql@2.15.27': + resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + + '@types/node@22.18.10': + resolution: {integrity: sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==} + + '@types/oracledb@6.5.2': + resolution: {integrity: sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==} + + '@types/pg-pool@2.0.6': + resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + + '@types/pg@8.15.5': + resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} + + '@types/pug@2.0.10': + resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} + + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + + '@types/snappyjs@0.7.1': + resolution: {integrity: sha512-OxjzJ6cQZstysMh6PEwZWmK9qlKZyezHJKOkcUkZDooSFuog2votUEKkxMaTq51UQF3cJkXKQ+XGlj4FSl8JQQ==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + + '@types/toposort@2.0.7': + resolution: {integrity: sha512-sQNk65vbC36+UixCkcky+dCr7MlflHcVILg1FVGqlUntsLFv9xd9ToWIVko/gTuin+cVe16t+2YubEFkhnSuPQ==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/uuid@8.3.4': + resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@typescript-eslint/eslint-plugin@6.21.0': + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@6.21.0': + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@6.21.0': + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/type-utils@6.21.0': + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@6.21.0': + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/typescript-estree@6.21.0': + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@6.21.0': + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@6.21.0': + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + + acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.8.16: + resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==} + hasBin: true + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.26.3: + resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + builtins@5.1.0: + resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001750: + resolution: {integrity: sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + code-red@1.0.4: + resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-convert@3.1.2: + resolution: {integrity: sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==} + engines: {node: '>=14.6'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-name@2.0.2: + resolution: {integrity: sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==} + engines: {node: '>=12.20'} + + color-string@2.1.2: + resolution: {integrity: sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==} + engines: {node: '>=18'} + + color@5.0.2: + resolution: {integrity: sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==} + engines: {node: '>=18'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + + cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + + data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + + dedent@1.7.0: + resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.237: + resolution: {integrity: sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@5.0.0: + resolution: {integrity: sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es6-promise@3.3.1: + resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} + + esbuild-plugin-copy@2.1.1: + resolution: {integrity: sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw==} + peerDependencies: + esbuild: '>= 0.14.0' + + esbuild-svelte@0.9.3: + resolution: {integrity: sha512-CgEcGY1r/d16+aggec3czoFBEBaYIrFOnMxpsO6fWNaNEqHregPN5DLAPZDqrL7rXDNplW+WMu8s3GMq9FqgJA==} + engines: {node: '>=18'} + peerDependencies: + esbuild: '>=0.17.0' + svelte: '>=4.2.1 <6' + + esbuild@0.25.11: + resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-standard-with-typescript@40.0.0: + resolution: {integrity: sha512-GXUJcwIXiTQaS3H4etv8a1lejVVdZYaxZNz3g7vt6GoJosQqMTurbmSC4FVGyHiGT/d1TjFr3+47A3xsHhsG+Q==} + deprecated: Please use eslint-config-love, instead. + peerDependencies: + '@typescript-eslint/eslint-plugin': ^6.4.0 + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: '^15.0.0 || ^16.0.0 ' + eslint-plugin-promise: ^6.0.0 + typescript: '*' + + eslint-config-standard@17.1.0: + resolution: {integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: '^15.0.0 || ^16.0.0 ' + eslint-plugin-promise: ^6.0.0 + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + eslint: '*' + peerDependenciesMeta: + eslint: + optional: true + + eslint-plugin-es@3.0.1: + resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + + eslint-plugin-es@4.1.0: + resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + + eslint-plugin-n@15.7.0: + resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==} + engines: {node: '>=12.22.0'} + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-node@11.1.0: + resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=5.16.0' + + eslint-plugin-promise@6.6.0: + resolution: {integrity: sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-svelte@2.46.1: + resolution: {integrity: sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + + eslint-utils@3.0.0: + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + + eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + + eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@5.3.2: + resolution: {integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-stream-rotator@0.6.1: + resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hash-it@6.0.0: + resolution: {integrity: sha512-KHzmSFx1KwyMPw0kXeeUD752q/Kfbzhy6dAZrjXV9kAIXGqzGvv8vhkUqj+2MGZldTo0IBpw6v7iWE7uxsvH0w==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + intl-messageformat@10.7.18: + resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-jsdom@29.7.0: + resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jwt-simple@0.5.6: + resolution: {integrity: sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==} + engines: {node: '>= 0.4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + known-css-properties@0.35.0: + resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lexorank@1.0.5: + resolution: {integrity: sha512-K1B/Yr/gIU0wm68hk/yB0p/mv6xM3ShD5aci42vOwcjof8slG8Kpo3Q7+1WTv7DaRHKWRgLPqrFDt+4GtuFAtA==} + + lib0@0.2.114: + resolution: {integrity: sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==} + engines: {node: '>=16'} + hasBin: true + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.5: + resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.23: + resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nwsapi@2.2.22: + resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-svelte@3.4.0: + resolution: {integrity: sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prosemirror-changeset@2.3.1: + resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==} + + prosemirror-codemark@0.4.2: + resolution: {integrity: sha512-4n+PnGQToa/vTjn0OiivUvE8/moLtguUAfry8UA4Q8p47MhqT2Qpf2zBLustX5Upi4mSp3z1ZYBqLLovZC6abA==} + peerDependencies: + prosemirror-inputrules: ^1.2.0 + prosemirror-model: ^1.18.1 + prosemirror-state: ^1.4.1 + prosemirror-view: ^1.26.2 + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.0: + resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==} + + prosemirror-history@1.4.1: + resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-markdown@1.13.2: + resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==} + + prosemirror-menu@1.2.5: + resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==} + + prosemirror-model@1.25.3: + resolution: {integrity: sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA==} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.3: + resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} + + prosemirror-tables@1.8.1: + resolution: {integrity: sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.10.4: + resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==} + + prosemirror-view@1.41.3: + resolution: {integrity: sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sander@0.5.1: + resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + snappyjs@0.7.0: + resolution: {integrity: sha512-u5iEEXkMe2EInQio6Wv9LWHOQYRDbD2O9hzS27GpT/lwfIQhTCnHCTqedqHIHe9ZcvQo+9au6vngQayipz1NYw==} + + sorcery@0.11.1: + resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte-eslint-parser@0.33.1: + resolution: {integrity: sha512-vo7xPGTlKBGdLH8T5L64FipvTrqv3OQRx9d2z5X05KKZDlF4rQk8KViZO4flKERY+5BiVdOh7zZ7JGJWo5P0uA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte-eslint-parser@0.43.0: + resolution: {integrity: sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte-preprocess@5.1.4: + resolution: {integrity: sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + '@babel/core': ^7.10.2 + coffeescript: ^2.5.1 + less: ^3.11.3 || ^4.0.0 + postcss: ^7 || ^8 + postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + pug: ^3.0.0 + sass: ^1.26.8 + stylus: ^0.55.0 + sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 + svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 + typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' + peerDependenciesMeta: + '@babel/core': + optional: true + coffeescript: + optional: true + less: + optional: true + postcss: + optional: true + postcss-load-config: + optional: true + pug: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + typescript: + optional: true + + svelte2tsx@0.7.45: + resolution: {integrity: sha512-cSci+mYGygYBHIZLHlm/jYlEc1acjAHqaQaDFHdEBpUueM9kSTnPpvPtSl5VkJOU1qSJ7h1K+6F/LIUYiqC8VA==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 + + svelte@4.2.20: + resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==} + engines: {node: '>=16'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-jest@29.4.5: + resolution: {integrity: sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + winston-daily-rotate-file@5.0.0: + resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==} + engines: {node: '>=8'} + peerDependencies: + winston: ^3 + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.18.3: + resolution: {integrity: sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==} + engines: {node: '>= 12.0.0'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y-prosemirror@1.3.7: + resolution: {integrity: sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.5.38 + + y-protocols@1.0.6: + resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yjs@13.6.27: + resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zeed-dom@0.15.1: + resolution: {integrity: sha512-dtZ0aQSFyZmoJS0m06/xBN1SazUBPL5HpzlAcs/KcRW0rzadYw12deQBjeMhGKMMeGEp7bA9vmikMLaO4exBcg==} + engines: {node: '>=14.13.1'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@0.2.3': {} + + '@colors/colors@1.6.0': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + + '@esbuild/aix-ppc64@0.25.11': + optional: true + + '@esbuild/android-arm64@0.25.11': + optional: true + + '@esbuild/android-arm@0.25.11': + optional: true + + '@esbuild/android-x64@0.25.11': + optional: true + + '@esbuild/darwin-arm64@0.25.11': + optional: true + + '@esbuild/darwin-x64@0.25.11': + optional: true + + '@esbuild/freebsd-arm64@0.25.11': + optional: true + + '@esbuild/freebsd-x64@0.25.11': + optional: true + + '@esbuild/linux-arm64@0.25.11': + optional: true + + '@esbuild/linux-arm@0.25.11': + optional: true + + '@esbuild/linux-ia32@0.25.11': + optional: true + + '@esbuild/linux-loong64@0.25.11': + optional: true + + '@esbuild/linux-mips64el@0.25.11': + optional: true + + '@esbuild/linux-ppc64@0.25.11': + optional: true + + '@esbuild/linux-riscv64@0.25.11': + optional: true + + '@esbuild/linux-s390x@0.25.11': + optional: true + + '@esbuild/linux-x64@0.25.11': + optional: true + + '@esbuild/netbsd-arm64@0.25.11': + optional: true + + '@esbuild/netbsd-x64@0.25.11': + optional: true + + '@esbuild/openbsd-arm64@0.25.11': + optional: true + + '@esbuild/openbsd-x64@0.25.11': + optional: true + + '@esbuild/openharmony-arm64@0.25.11': + optional: true + + '@esbuild/sunos-x64@0.25.11': + optional: true + + '@esbuild/win32-arm64@0.25.11': + optional: true + + '@esbuild/win32-ia32@0.25.11': + optional: true + + '@esbuild/win32-x64@0.25.11': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@formatjs/ecma402-abstract@2.3.6': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.4': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/icu-skeleton-parser': 1.8.16 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.16': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.2': + dependencies: + tslib: 2.8.1 + + '@grpc/grpc-js@1.14.0': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + + '@hcengineering/platform-rig@0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3))': + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: 0.25.11 + esbuild-plugin-copy: 2.1.1(esbuild@0.25.11) + esbuild-svelte: 0.9.3(esbuild@0.25.11)(svelte@4.2.20) + eslint: 8.57.1 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: 2.32.0(eslint@8.57.1) + eslint-plugin-n: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + prettier: 3.6.2 + prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@4.2.20) + svelte: 4.2.20 + svelte-eslint-parser: 0.33.1(svelte@4.2.20) + svelte-preprocess: 5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(svelte@4.2.20)(typescript@5.9.3) + svelte2tsx: 0.7.45(svelte@4.2.20)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - '@babel/core' + - coffeescript + - less + - postcss + - postcss-load-config + - pug + - sass + - stylus + - sugarss + - supports-color + - ts-node + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.18.10 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 22.18.10 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.28.4 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.18.10 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@js-sdsl/ordered-map@4.4.2': {} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@opentelemetry/api-logs@0.203.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/auto-instrumentations-node@0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-lambda': 0.54.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-sdk': 0.58.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-bunyan': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cassandra-driver': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cucumber': 0.19.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.21.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dns': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.48.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.51.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-grpc': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.51.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.13.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.48.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.51.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.48.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-memcached': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-net': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-oracledb': 0.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.56.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pino': 0.50.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-restify': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-router': 0.48.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-runtime-node': 0.17.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-socket.io': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.22.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.14.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-winston': 0.48.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-alibaba-cloud': 0.31.9(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-aws': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-azure': 0.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-container': 0.7.9(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-gcp': 0.37.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-prometheus@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/id-generator-aws-xray@2.0.3(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/instrumentation-amqplib@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-lambda@0.54.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/aws-lambda': 8.10.152 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-sdk@0.58.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-bunyan@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@types/bunyan': 1.8.11 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cassandra-driver@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cucumber@0.19.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.21.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dns@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.52.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fastify@0.48.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.23.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.51.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-grpc@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.51.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.38.2 + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.13.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.48.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.51.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.48.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-memcached@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/memcached': 2.2.10 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/mysql': 2.15.27 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-nestjs-core@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-net@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-oracledb@0.29.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/oracledb': 6.5.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.56.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) + '@types/pg': 8.15.5 + '@types/pg-pool': 2.0.6 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pino@0.50.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis@0.52.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.38.2 + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-restify@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-router@0.48.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-runtime-node@0.17.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-socket.io@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.22.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.14.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-winston@0.48.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-grpc-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/redis-common@0.38.2': {} + + '@opentelemetry/resource-detector-alibaba-cloud@0.31.9(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/resource-detector-aws@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/resource-detector-azure@0.10.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/resource-detector-container@0.7.9(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/resource-detector-gcp@0.37.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + gcp-metadata: 6.1.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/resources@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/sdk-logs@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-node@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-node@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/semantic-conventions@1.37.0': {} + + '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@remirror/core-constants@3.0.0': {} + + '@rtsao/scc@1.1.0': {} + + '@sinclair/typebox@0.27.8': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.2 + text-hex: 1.0.0 + + '@tiptap/core@2.26.3(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/pm': 2.26.3 + + '@tiptap/extension-blockquote@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-bold@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-bullet-list@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-code-block@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + + '@tiptap/extension-code@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-document@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-dropcursor@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + + '@tiptap/extension-gapcursor@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + + '@tiptap/extension-hard-break@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-heading@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-highlight@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-history@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + + '@tiptap/extension-horizontal-rule@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + + '@tiptap/extension-italic@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + linkifyjs: 4.3.2 + + '@tiptap/extension-list-item@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-mention@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(@tiptap/suggestion@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + '@tiptap/suggestion': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + + '@tiptap/extension-ordered-list@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-paragraph@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-strike@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-table-cell@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-table-header@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-table-row@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-table@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + + '@tiptap/extension-task-item@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + + '@tiptap/extension-task-list@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-text-align@2.11.9(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-text-style@2.11.9(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-text-style@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-text@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-typography@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-underline@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/html@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + zeed-dom: 0.15.1 + + '@tiptap/pm@2.26.3': + dependencies: + prosemirror-changeset: 2.3.1 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.0 + prosemirror-history: 1.4.1 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.2 + prosemirror-menu: 1.2.5 + prosemirror-model: 1.25.3 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.3 + prosemirror-tables: 1.8.1 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3) + prosemirror-transform: 1.10.4 + prosemirror-view: 1.41.3 + + '@tiptap/starter-kit@2.26.3': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/extension-blockquote': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-bold': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-bullet-list': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-code': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-code-block': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-document': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-dropcursor': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-gapcursor': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-hard-break': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-heading': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-history': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-horizontal-rule': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-italic': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-list-item': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-ordered-list': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-paragraph': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-strike': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-text': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-text-style': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/pm': 2.26.3 + + '@tiptap/suggestion@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + + '@tootallnate/once@2.0.0': {} + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/aws-lambda@8.10.152': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/bunyan@1.8.11': + dependencies: + '@types/node': 22.18.10 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.18.10 + + '@types/estree@1.0.8': {} + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.18.10 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/jsdom@20.0.1': + dependencies: + '@types/node': 22.18.10 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/linkify-it@3.0.5': {} + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@13.0.9': + dependencies: + '@types/linkify-it': 3.0.5 + '@types/mdurl': 1.0.5 + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@1.0.5': {} + + '@types/mdurl@2.0.0': {} + + '@types/memcached@2.2.10': + dependencies: + '@types/node': 22.18.10 + + '@types/mysql@2.15.27': + dependencies: + '@types/node': 22.18.10 + + '@types/node@22.18.10': + dependencies: + undici-types: 6.21.0 + + '@types/oracledb@6.5.2': + dependencies: + '@types/node': 22.18.10 + + '@types/pg-pool@2.0.6': + dependencies: + '@types/pg': 8.15.5 + + '@types/pg@8.15.5': + dependencies: + '@types/node': 22.18.10 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + + '@types/pug@2.0.10': {} + + '@types/semver@7.7.1': {} + + '@types/snappyjs@0.7.1': {} + + '@types/stack-utils@2.0.3': {} + + '@types/tedious@4.0.14': + dependencies: + '@types/node': 22.18.10 + + '@types/toposort@2.0.7': {} + + '@types/tough-cookie@4.0.5': {} + + '@types/triple-beam@1.3.5': {} + + '@types/uuid@8.3.4': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.18.10 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + semver: 7.7.3 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + eslint: 8.57.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@6.21.0': {} + + '@typescript-eslint/typescript-estree@6.21.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.7.3 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.1 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + eslint: 8.57.1 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.3.0': {} + + abab@2.0.6: {} + + acorn-globals@7.0.1: + dependencies: + acorn: 8.15.0 + acorn-walk: 8.3.4 + + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array-union@2.1.0: {} + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + async-function@1.0.0: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axobject-query@4.1.0: {} + + babel-jest@29.7.0(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.28.4) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4) + + babel-preset-jest@29.6.3(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.8.16: {} + + bignumber.js@9.3.1: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.26.3: + dependencies: + baseline-browser-mapping: 2.8.16 + caniuse-lite: 1.0.30001750 + electron-to-chromium: 1.5.237 + node-releases: 2.0.23 + update-browserslist-db: 1.1.3(browserslist@4.26.3) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-crc32@1.0.0: {} + + buffer-from@1.1.2: {} + + builtins@5.1.0: + dependencies: + semver: 7.7.3 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001750: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.3: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + co@4.6.0: {} + + code-red@1.0.4: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@types/estree': 1.0.8 + acorn: 8.15.0 + estree-walker: 3.0.3 + periscopic: 3.1.0 + + collect-v8-coverage@1.0.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-convert@3.1.2: + dependencies: + color-name: 2.0.2 + + color-name@1.1.4: {} + + color-name@2.0.2: {} + + color-string@2.1.2: + dependencies: + color-name: 2.0.2 + + color@5.0.2: + dependencies: + color-convert: 3.1.2 + color-string: 2.1.2 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + create-jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + create-require@1.1.1: {} + + crelt@1.0.6: {} + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + cssesc@3.0.0: {} + + cssom@0.3.8: {} + + cssom@0.5.0: {} + + cssstyle@2.3.0: + dependencies: + cssom: 0.3.8 + + data-urls@3.0.2: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + dedent-js@1.0.1: {} + + dedent@1.7.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + detect-indent@6.1.0: {} + + detect-libc@2.1.2: + optional: true + + detect-newline@3.1.0: {} + + diff-sequences@29.6.3: {} + + diff@4.0.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domexception@4.0.0: + dependencies: + webidl-conversions: 7.0.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.237: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + enabled@2.0.0: {} + + entities@4.5.0: {} + + entities@5.0.0: {} + + entities@6.0.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es6-promise@3.3.1: {} + + esbuild-plugin-copy@2.1.1(esbuild@0.25.11): + dependencies: + chalk: 4.1.2 + chokidar: 3.6.0 + esbuild: 0.25.11 + fs-extra: 10.1.0 + globby: 11.1.0 + + esbuild-svelte@0.9.3(esbuild@0.25.11)(svelte@4.2.20): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + esbuild: 0.25.11 + svelte: 4.2.20 + + esbuild@0.25.11: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.11 + '@esbuild/android-arm': 0.25.11 + '@esbuild/android-arm64': 0.25.11 + '@esbuild/android-x64': 0.25.11 + '@esbuild/darwin-arm64': 0.25.11 + '@esbuild/darwin-x64': 0.25.11 + '@esbuild/freebsd-arm64': 0.25.11 + '@esbuild/freebsd-x64': 0.25.11 + '@esbuild/linux-arm': 0.25.11 + '@esbuild/linux-arm64': 0.25.11 + '@esbuild/linux-ia32': 0.25.11 + '@esbuild/linux-loong64': 0.25.11 + '@esbuild/linux-mips64el': 0.25.11 + '@esbuild/linux-ppc64': 0.25.11 + '@esbuild/linux-riscv64': 0.25.11 + '@esbuild/linux-s390x': 0.25.11 + '@esbuild/linux-x64': 0.25.11 + '@esbuild/netbsd-arm64': 0.25.11 + '@esbuild/netbsd-x64': 0.25.11 + '@esbuild/openbsd-arm64': 0.25.11 + '@esbuild/openbsd-x64': 0.25.11 + '@esbuild/openharmony-arm64': 0.25.11 + '@esbuild/sunos-x64': 0.25.11 + '@esbuild/win32-arm64': 0.25.11 + '@esbuild/win32-ia32': 0.25.11 + '@esbuild/win32-x64': 0.25.11 + + escalade@3.2.0: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-compat-utils@0.5.1(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + semver: 7.7.3 + + eslint-config-standard-with-typescript@40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(eslint@8.57.1) + eslint-plugin-n: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: 6.6.0(eslint@8.57.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-plugin-import: 2.32.0(eslint@8.57.1) + eslint-plugin-n: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: 6.6.0(eslint@8.57.1) + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + + eslint-module-utils@2.12.1(eslint@8.57.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + eslint: 8.57.1 + + eslint-plugin-es@3.0.1(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + + eslint-plugin-es@4.1.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + + eslint-plugin-import@2.32.0(eslint@8.57.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + + eslint-plugin-n@15.7.0(eslint@8.57.1): + dependencies: + builtins: 5.1.0 + eslint: 8.57.1 + eslint-plugin-es: 4.1.0(eslint@8.57.1) + eslint-utils: 3.0.0(eslint@8.57.1) + ignore: 5.3.2 + is-core-module: 2.16.1 + minimatch: 3.1.2 + resolve: 1.22.10 + semver: 7.7.3 + + eslint-plugin-node@11.1.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-plugin-es: 3.0.1(eslint@8.57.1) + eslint-utils: 2.1.0 + ignore: 5.3.2 + minimatch: 3.1.2 + resolve: 1.22.10 + semver: 6.3.1 + + eslint-plugin-promise@6.6.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-svelte@2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@jridgewell/sourcemap-codec': 1.5.5 + eslint: 8.57.1 + eslint-compat-utils: 0.5.1(eslint@8.57.1) + esutils: 2.0.3 + known-css-properties: 0.35.0 + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + postcss-safe-parser: 6.0.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + semver: 7.7.3 + svelte-eslint-parser: 0.43.0(svelte@4.2.20) + optionalDependencies: + svelte: 4.2.20 + transitivePeerDependencies: + - ts-node + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-utils@2.1.0: + dependencies: + eslint-visitor-keys: 1.3.0 + + eslint-utils@3.0.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 2.1.0 + + eslint-visitor-keys@1.3.0: {} + + eslint-visitor-keys@2.1.0: {} + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-equals@5.3.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fecha@4.2.3: {} + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-stream-rotator@0.6.1: + dependencies: + moment: 2.30.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + fn.name@1.1.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + forwarded-parse@2.1.2: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + google-logging-utils@0.0.2: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hash-it@6.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-encoding-sniffer@3.0.0: + dependencies: + whatwg-encoding: 2.0.0 + + html-escaper@2.0.2: {} + + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-in-the-middle@1.15.0: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + intl-messageformat@10.7.18: + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.4 + tslib: 2.8.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-potential-custom-element-name@1.0.1: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isomorphic.js@0.2.5: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.0 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.28.4 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.4) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.18.10 + ts-node: 10.9.2(@types/node@22.18.10)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-jsdom@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.1 + '@types/node': 22.18.10 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.18.10 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.10 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.28.4 + '@babel/generator': 7.28.3 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) + '@babel/types': 7.28.4 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.18.10 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.18.10 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsdom@20.0.3: + dependencies: + abab: 2.0.6 + acorn: 8.15.0 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.6.0 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.4 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.22 + parse5: 7.3.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.18.3 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jwt-simple@0.5.6: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + known-css-properties@0.35.0: {} + + kuler@2.0.0: {} + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lexorank@1.0.5: {} + + lib0@0.2.114: + dependencies: + isomorphic.js: 0.2.5 + + lilconfig@2.1.0: {} + + lines-and-columns@1.2.4: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.3.2: {} + + locate-character@3.0.0: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.camelcase@4.3.0: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + long@5.3.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + math-intrinsics@1.1.0: {} + + mdn-data@2.0.30: {} + + mdurl@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + min-indent@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + module-details-from-path@1.0.4: {} + + moment@2.30.1: {} + + ms@2.1.3: {} + + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.5: + optionalDependencies: + msgpackr-extract: 3.0.3 + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-int64@0.4.0: {} + + node-releases@2.0.23: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nwsapi@2.2.22: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + orderedmap@2.1.1: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + periscopic@3.1.0: + dependencies: + '@types/estree': 1.0.8 + estree-walker: 3.0.3 + is-reference: 3.0.3 + + pg-int8@1.0.1: {} + + pg-protocol@1.10.3: {} + + 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.7 + postgres-interval: 1.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + possible-typed-array-names@1.1.0: {} + + postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.2 + optionalDependencies: + postcss: 8.5.6 + ts-node: 10.9.2(@types/node@22.18.10)(typescript@5.9.3) + + postcss-safe-parser@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres@3.4.7: {} + + prelude-ls@1.2.1: {} + + prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@4.2.20): + dependencies: + prettier: 3.6.2 + svelte: 4.2.20 + + prettier@3.6.2: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prosemirror-changeset@2.3.1: + dependencies: + prosemirror-transform: 1.10.4 + + prosemirror-codemark@0.4.2(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3): + dependencies: + prosemirror-inputrules: 1.5.1 + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-view: 1.41.3 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.3 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.41.3 + + prosemirror-gapcursor@1.4.0: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-view: 1.41.3 + + prosemirror-history@1.4.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.41.3 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.3 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.2: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + prosemirror-model: 1.25.3 + + prosemirror-menu@1.2.5: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.4.1 + prosemirror-state: 1.4.3 + + prosemirror-model@1.25.3: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.3 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + + prosemirror-state@1.4.3: + dependencies: + prosemirror-model: 1.25.3 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.41.3 + + prosemirror-tables@1.8.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.41.3 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-view: 1.41.3 + + prosemirror-transform@1.10.4: + dependencies: + prosemirror-model: 1.25.3 + + prosemirror-view@1.41.3: + dependencies: + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.18.10 + long: 5.3.2 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + react-is@18.3.1: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpp@3.2.0: {} + + require-directory@2.1.1: {} + + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + requires-port@1.0.0: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rope-sequence@1.3.4: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sander@0.5.1: + dependencies: + es6-promise: 3.3.1 + graceful-fs: 4.2.11 + mkdirp: 0.5.6 + rimraf: 2.7.1 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scule@1.3.0: {} + + semver@6.3.1: {} + + semver@7.7.3: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + snappyjs@0.7.0: {} + + sorcery@0.11.1: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + buffer-crc32: 1.0.0 + minimist: 1.2.8 + sander: 0.5.1 + + source-map-js@1.2.1: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + sprintf-js@1.0.3: {} + + stack-trace@0.0.10: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svelte-eslint-parser@0.33.1(svelte@4.2.20): + dependencies: + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + postcss: 8.5.6 + postcss-scss: 4.0.9(postcss@8.5.6) + optionalDependencies: + svelte: 4.2.20 + + svelte-eslint-parser@0.43.0(svelte@4.2.20): + dependencies: + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + postcss: 8.5.6 + postcss-scss: 4.0.9(postcss@8.5.6) + optionalDependencies: + svelte: 4.2.20 + + svelte-preprocess@5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(svelte@4.2.20)(typescript@5.9.3): + dependencies: + '@types/pug': 2.0.10 + detect-indent: 6.1.0 + magic-string: 0.30.19 + sorcery: 0.11.1 + strip-indent: 3.0.0 + svelte: 4.2.20 + optionalDependencies: + '@babel/core': 7.28.4 + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + typescript: 5.9.3 + + svelte2tsx@0.7.45(svelte@4.2.20)(typescript@5.9.3): + dependencies: + dedent-js: 1.0.1 + scule: 1.3.0 + svelte: 4.2.20 + typescript: 5.9.3 + + svelte@4.2.20: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + code-red: 1.0.4 + css-tree: 2.3.1 + estree-walker: 3.0.3 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.19 + periscopic: 3.1.0 + + symbol-tree@3.2.4: {} + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-hex@1.0.0: {} + + text-table@0.2.0: {} + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toposort@2.0.2: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@0.0.3: {} + + tr46@3.0.0: + dependencies: + punycode: 2.3.1 + + triple-beam@1.4.1: {} + + ts-api-utils@1.4.3(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-jest@29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.4 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.4) + esbuild: 0.25.11 + jest-util: 29.7.0 + + ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.18.10 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + uc.micro@2.1.0: {} + + uglify-js@3.19.3: + optional: true + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + update-browserslist-db@1.1.3(browserslist@4.26.3): + dependencies: + browserslist: 4.26.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + util-deprecate@1.0.2: {} + + uuid@8.3.2: {} + + uuid@9.0.1: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + w3c-keyname@2.2.8: {} + + w3c-xmlserializer@4.0.0: + dependencies: + xml-name-validator: 4.0.0 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + webidl-conversions@3.0.1: {} + + webidl-conversions@7.0.0: {} + + whatwg-encoding@2.0.0: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@3.0.0: {} + + whatwg-url@11.0.0: + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + winston-daily-rotate-file@5.0.0(winston@3.18.3): + dependencies: + file-stream-rotator: 0.6.1 + object-hash: 3.0.0 + triple-beam: 1.4.1 + winston: 3.18.3 + winston-transport: 4.9.0 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.18.3: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + ws@8.18.3: {} + + xml-name-validator@4.0.0: {} + + xmlchars@2.2.0: {} + + xtend@4.0.2: {} + + y-prosemirror@1.3.7(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27): + dependencies: + lib0: 0.2.114 + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-view: 1.41.3 + y-protocols: 1.0.6(yjs@13.6.27) + yjs: 13.6.27 + + y-protocols@1.0.6(yjs@13.6.27): + dependencies: + lib0: 0.2.114 + yjs: 13.6.27 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@1.10.2: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yjs@13.6.27: + dependencies: + lib0: 0.2.114 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + zeed-dom@0.15.1: + dependencies: + css-what: 6.2.2 + entities: 5.0.0 diff --git a/foundations/core/common/config/rush/repo-state.json b/foundations/core/common/config/rush/repo-state.json new file mode 100644 index 0000000000..0e7b144099 --- /dev/null +++ b/foundations/core/common/config/rush/repo-state.json @@ -0,0 +1,4 @@ +// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. +{ + "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" +} diff --git a/foundations/core/common/config/rush/rush-plugins.json b/foundations/core/common/config/rush/rush-plugins.json new file mode 100644 index 0000000000..752e373213 --- /dev/null +++ b/foundations/core/common/config/rush/rush-plugins.json @@ -0,0 +1,29 @@ +/** + * This configuration file manages Rush's plugin feature. + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugins.schema.json", + "plugins": [ + /** + * Each item configures a plugin to be loaded by Rush. + */ + // { + // /** + // * The name of the NPM package that provides the plugin. + // */ + // "packageName": "@scope/my-rush-plugin", + // /** + // * The name of the plugin. This can be found in the "pluginName" + // * field of the "rush-plugin-manifest.json" file in the NPM package folder. + // */ + // "pluginName": "my-plugin-name", + // /** + // * The name of a Rush autoinstaller that will be used for installation, which + // * can be created using "rush init-autoinstaller". Add the plugin's NPM package + // * to the package.json "dependencies" of your autoinstaller, then run + // * "rush update-autoinstaller". + // */ + // "autoinstallerName": "rush-plugins" + // } + ] +} \ No newline at end of file diff --git a/foundations/core/common/config/rush/subspaces.json b/foundations/core/common/config/rush/subspaces.json new file mode 100644 index 0000000000..d3c3ae8c51 --- /dev/null +++ b/foundations/core/common/config/rush/subspaces.json @@ -0,0 +1,35 @@ +/** + * This configuration file manages the experimental "subspaces" feature for Rush, + * which allows multiple PNPM lockfiles to be used in a single Rush workspace. + * For full documentation, please see https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json", + + /** + * Set this flag to "true" to enable usage of subspaces. + */ + "subspacesEnabled": false, + + /** + * (DEPRECATED) This is a temporary workaround for migrating from an earlier prototype + * of this feature: https://github.com/microsoft/rushstack/pull/3481 + * It allows subspaces with only one project to store their config files in the project folder. + */ + "splitWorkspaceCompatibility": false, + + /** + * When a command such as "rush update" is invoked without the "--subspace" or "--to" + * parameters, Rush will install all subspaces. In a huge monorepo with numerous subspaces, + * this would be extremely slow. Set "preventSelectingAllSubspaces" to true to avoid this + * mistake by always requiring selection parameters for commands such as "rush update". + */ + "preventSelectingAllSubspaces": false, + + /** + * The list of subspace names, which should be lowercase alphanumeric words separated by + * hyphens, for example "my-subspace". The corresponding config files will have paths + * such as "common/config/subspaces/my-subspace/package-lock.yaml". + */ + "subspaceNames": [] +} diff --git a/foundations/core/common/config/rush/version-policies.json b/foundations/core/common/config/rush/version-policies.json new file mode 100644 index 0000000000..00501e9e33 --- /dev/null +++ b/foundations/core/common/config/rush/version-policies.json @@ -0,0 +1,102 @@ +/** + * This is configuration file is used for advanced publishing configurations with Rush. + * More documentation is available on the Rush website: https://rushjs.io + */ + +/** + * A list of version policy definitions. A "version policy" is a custom package versioning + * strategy that affects "rush change", "rush version", and "rush publish". The strategy applies + * to a set of projects that are specified using the "versionPolicyName" field in rush.json. + */ +[ + // { + // /** + // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). + // * + // * The "lockStepVersion" mode specifies that the projects will use "lock-step versioning". This + // * strategy is appropriate for a set of packages that act as selectable components of a + // * unified product. The entire set of packages are always published together, and always share + // * the same NPM version number. When the packages depend on other packages in the set, the + // * SemVer range is usually restricted to a single version. + // */ + // "definitionName": "lockStepVersion", + // + // /** + // * (Required) The name that will be used for the "versionPolicyName" field in rush.json. + // * This name is also used command-line parameters such as "--version-policy" + // * and "--to-version-policy". + // */ + // "policyName": "MyBigFramework", + // + // /** + // * (Required) The current version. All packages belonging to the set should have this version + // * in the current branch. When bumping versions, Rush uses this to determine the next version. + // * (The "version" field in package.json is NOT considered.) + // */ + // "version": "1.0.0", + // + // /** + // * (Required) The type of bump that will be performed when publishing the next release. + // * When creating a release branch in Git, this field should be updated according to the + // * type of release. + // * + // * Valid values are: "prerelease", "preminor", "minor", "patch", "major" + // */ + // "nextBump": "prerelease", + // + // /** + // * (Optional) If specified, all packages in the set share a common CHANGELOG.md file. + // * This file is stored with the specified "main" project, which must be a member of the set. + // * + // * If this field is omitted, then a separate CHANGELOG.md file will be maintained for each + // * package in the set. + // */ + // "mainProject": "my-app", + // + // /** + // * (Optional) If enabled, the "rush change" command will prompt the user for their email address + // * and include it in the JSON change files. If an organization maintains multiple repos, tracking + // * this contact information may be useful for a service that automatically upgrades packages and + // * needs to notify engineers whose change may be responsible for a downstream build break. It might + // * also be useful for crediting contributors. Rush itself does not do anything with the collected + // * email addresses. The default value is "false". + // */ + // // "includeEmailInChangeFile": true + // }, + // + { + /** + * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). + * + * The "individualVersion" mode specifies that the projects will use "individual versioning". + * This is the typical NPM model where each package has an independent version number + * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the + * packages, they otherwise don't have any special relationship. The version bumping will + * depend on how developers answer the "rush change" questions for each package that + * is changed. + */ + "definitionName": "individualVersion", + + "policyName": "Huly.core", + + /** + * (Optional) This can be used to enforce that all packages in the set must share a common + * major version number, e.g. because they are from the same major release branch. + * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes + * inappropriately. The minor/patch version parts will be bumped independently according + * to the types of changes made to each project, according to the "rush change" command. + */ + "lockedMajor": 0.7, + + /** + * (Optional) When publishing is managed by Rush, by default the "rush change" command will + * request changes for any projects that are modified by a pull request. These change entries + * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates + * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects + * belonging to this version policy. + */ + "exemptFromRushChange": false + + // "includeEmailInChangeFile": true + } +] diff --git a/foundations/core/common/git-hooks/commit-msg.sample b/foundations/core/common/git-hooks/commit-msg.sample new file mode 100644 index 0000000000..59cacb80ca --- /dev/null +++ b/foundations/core/common/git-hooks/commit-msg.sample @@ -0,0 +1,25 @@ +#!/bin/sh +# +# This is an example Git hook for use with Rush. To enable this hook, rename this file +# to "commit-msg" and then run "rush install", which will copy it from common/git-hooks +# to the .git/hooks folder. +# +# TO LEARN MORE ABOUT GIT HOOKS +# +# The Git documentation is here: https://git-scm.com/docs/githooks +# Some helpful resources: https://githooks.com +# +# ABOUT THIS EXAMPLE +# +# The commit-msg hook is called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero status after issuing +# an appropriate message if it wants to stop the commit. The hook is allowed to edit +# the commit message file. + +# This example enforces that commit message should contain a minimum amount of +# description text. +if [ `cat $1 | wc -w` -lt 3 ]; then + echo "" + echo "Invalid commit message: The message must contain at least 3 words." + exit 1 +fi diff --git a/foundations/core/common/scripts/generate-coverage-html.js b/foundations/core/common/scripts/generate-coverage-html.js new file mode 100644 index 0000000000..6bc54b62cb --- /dev/null +++ b/foundations/core/common/scripts/generate-coverage-html.js @@ -0,0 +1,153 @@ +#!/usr/bin/env node +const fs = require('fs') +const path = require('path') + +const [, , inFile = 'coverage/lcov.info', outDir = 'coverage/html'] = process.argv + +if (!fs.existsSync(inFile)) { + console.error('Input lcov not found:', inFile) + process.exit(1) +} + +const lcovParse = require('lcov-parse') +const libCoverage = require('istanbul-lib-coverage') +const reports = require('istanbul-reports') +const libReport = require('istanbul-lib-report') + +const data = fs.readFileSync(inFile, 'utf8') + +// build repo file index to resolve source files +const root = process.cwd() +const ignoreDirs = new Set(['node_modules', '.git', 'coverage', 'lib', 'dist', 'types', '.rush', 'temp', 'pnpm-store']) +const repoFiles = [] +function walk(dir) { + const items = fs.readdirSync(dir, { withFileTypes: true }) + for (const it of items) { + if (it.isDirectory()) { + if (ignoreDirs.has(it.name)) continue + if (it.name.startsWith('.')) continue + try { + walk(path.join(dir, it.name)) + } catch (e) {} + } else if (it.isFile()) { + repoFiles.push(path.join(dir, it.name)) + } + } +} +try { + walk(root) +} catch (e) {} + +lcovParse(data, (err, parsed) => { + if (err) { + console.error('lcov-parse error:', err) + process.exit(1) + } + + const map = libCoverage.createCoverageMap({}) + for (const file of parsed) { + // parsed entries include 'file', 'lines', 'functions', 'branches' + const coverage = { + path: file.file, + statementMap: {}, + fnMap: {}, + branchMap: {}, + s: {}, + f: {}, + b: {} + } + + // The lcov parser gives line coverage data; create synthetic statement entries per line + if (file.lines && file.lines.details) { + let idx = 0 + for (const d of file.lines.details) { + idx++ + const key = String(idx) + coverage.statementMap[key] = { start: { line: d.line, column: 0 }, end: { line: d.line, column: 0 } } + coverage.s[key] = d.hit + } + } + + // functions and branches are ignored for more accurate tools; keep minimal + map.addFileCoverage(coverage) + } + + // custom source finder: try absolute, repo-relative, and suffix matches + const sourceFinder = (filePath) => { + try { + if (!global.__seenPaths) global.__seenPaths = [] + if (global.__seenPaths.length < 500) global.__seenPaths.push(filePath) + if (global.__seenPaths.length === 500 && !global.__seenLogged) { + console.error('sourceFinder seen paths (sample):\n', global.__seenPaths.join('\n')) + global.__seenLogged = true + } + if (global.__seenPaths.length <= 200) console.error('sourceFinder request:', filePath) + } catch (e) {} + try { + if (path.isAbsolute(filePath) && fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf8') + const abs1 = path.resolve(root, filePath) + if (fs.existsSync(abs1)) return fs.readFileSync(abs1, 'utf8') + // try suffix match + const found = repoFiles.find((p) => p.endsWith(path.sep + filePath) || p.endsWith(filePath)) + if (found) return fs.readFileSync(found, 'utf8') + // debug unresolved + if (!found) { + try { + if (!global.__unresolved) global.__unresolved = new Set() + if (global.__unresolved.size < 200) global.__unresolved.add(filePath) + } catch (e) {} + } + } catch (e) { + // ignore and return null below + } + return null + } + + const context = libReport.createContext({ dir: outDir, coverageMap: map, sourceFinder }) + const report = reports.create('html', {}) + report.execute(context) + if (global.__unresolved && global.__unresolved.size) { + console.error('Unresolved filePath samples:\n', Array.from(global.__unresolved).slice(0, 50).join('\n')) + } + console.log('HTML report generated in', outDir) + // Post-process HTML files: if any report page contains the 'Unable to lookup source' placeholder, + // replace it with the actual source file contents when we can resolve it. + try { + for (const file of parsed) { + const srcAbs = file.file + // normalize key as used by report (from last '/src/' onward) if present + let key + const idx = srcAbs.lastIndexOf(path.sep + 'src' + path.sep) + if (idx !== -1) key = srcAbs.slice(idx + 1) + else key = path.basename(srcAbs) + + const htmlPath = path.join(outDir, key + '.html') + if (!fs.existsSync(htmlPath)) continue + let html = fs.readFileSync(htmlPath, 'utf8') + if (!html.includes('Unable to lookup source')) continue + // read source + let src + try { + src = fs.readFileSync(srcAbs, 'utf8') + } catch (e) { + src = null + } + if (!src) { + // try suffix match in repoFiles + const found = repoFiles.find((p) => p.endsWith(path.sep + key) || p.endsWith(key)) + if (found) src = fs.readFileSync(found, 'utf8') + } + if (!src) continue + // escape HTML + const esc = src.replace(/&/g, '&').replace(//g, '>') + // replace the first prettyprint

...
block that contains 'Unable to lookup source' + html = html.replace( + /
${esc}
` + ) + fs.writeFileSync(htmlPath, html, 'utf8') + } + } catch (e) { + console.error('post-process html error', e) + } +}) diff --git a/foundations/core/common/scripts/install-run-rush-pnpm.js b/foundations/core/common/scripts/install-run-rush-pnpm.js new file mode 100644 index 0000000000..2356649f4e --- /dev/null +++ b/foundations/core/common/scripts/install-run-rush-pnpm.js @@ -0,0 +1,31 @@ +// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. +// +// This script is intended for usage in an automated build environment where the Rush command may not have +// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush +// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the +// rush-pnpm command. +// +// An example usage would be: +// +// node common/scripts/install-run-rush-pnpm.js pnpm-command +// +// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. + +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +var __webpack_exports__ = {}; +/*!*****************************************************!*\ + !*** ./lib-esnext/scripts/install-run-rush-pnpm.js ***! + \*****************************************************/ + +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +require('./install-run-rush'); +//# sourceMappingURL=install-run-rush-pnpm.js.map +module.exports = __webpack_exports__; +/******/ })() +; +//# sourceMappingURL=install-run-rush-pnpm.js.map \ No newline at end of file diff --git a/foundations/core/common/scripts/install-run-rush.js b/foundations/core/common/scripts/install-run-rush.js new file mode 100644 index 0000000000..ef1d697f9c --- /dev/null +++ b/foundations/core/common/scripts/install-run-rush.js @@ -0,0 +1,218 @@ +// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. +// +// This script is intended for usage in an automated build environment where the Rush command may not have +// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush +// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it. +// An example usage would be: +// +// node common/scripts/install-run-rush.js install +// +// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. + +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ({ + +/***/ 16928: +/*!***********************!*\ + !*** external "path" ***! + \***********************/ +/***/ ((module) => { + +module.exports = require("path"); + +/***/ }), + +/***/ 179896: +/*!*********************!*\ + !*** external "fs" ***! + \*********************/ +/***/ ((module) => { + +module.exports = require("fs"); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => (module['default']) : +/******/ () => (module); +/******/ __webpack_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. +(() => { +/*!************************************************!*\ + !*** ./lib-esnext/scripts/install-run-rush.js ***! + \************************************************/ +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 16928); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +/* eslint-disable no-console */ + + +const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run'); +const PACKAGE_NAME = '@microsoft/rush'; +const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION'; +const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH'; +function _getRushVersion(logger) { + const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION]; + if (rushPreviewVersion !== undefined) { + logger.info(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`); + return rushPreviewVersion; + } + const rushJsonFolder = findRushJsonFolder(); + const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME); + try { + const rushJsonContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(rushJsonPath, 'utf-8'); + // Use a regular expression to parse out the rushVersion value because rush.json supports comments, + // but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script. + const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/); + return rushJsonMatches[1]; + } + catch (e) { + throw new Error(`Unable to determine the required version of Rush from ${RUSH_JSON_FILENAME} (${rushJsonFolder}). ` + + `The 'rushVersion' field is either not assigned in ${RUSH_JSON_FILENAME} or was specified ` + + 'using an unexpected syntax.'); + } +} +function _getBin(scriptName) { + switch (scriptName.toLowerCase()) { + case 'install-run-rush-pnpm.js': + return 'rush-pnpm'; + case 'install-run-rushx.js': + return 'rushx'; + default: + return 'rush'; + } +} +function _run() { + const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv; + // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the + // appropriate binary inside the rush package to run + const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath); + const bin = _getBin(scriptName); + if (!nodePath || !scriptPath) { + throw new Error('Unexpected exception: could not detect node path or script path'); + } + let commandFound = false; + let logger = { info: console.log, error: console.error }; + for (const arg of packageBinArgs) { + if (arg === '-q' || arg === '--quiet') { + // The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress + // any normal informational/diagnostic information printed during startup. + // + // To maintain the same user experience, the install-run* scripts pass along this + // flag but also use it to suppress any diagnostic information normally printed + // to stdout. + logger = { + info: () => { }, + error: console.error + }; + } + else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') { + // We either found something that looks like a command (i.e. - doesn't start with a "-"), + // or we found the -h/--help flag, which can be run without a command + commandFound = true; + } + } + if (!commandFound) { + console.log(`Usage: ${scriptName} [args...]`); + if (scriptName === 'install-run-rush-pnpm.js') { + console.log(`Example: ${scriptName} pnpm-command`); + } + else if (scriptName === 'install-run-rush.js') { + console.log(`Example: ${scriptName} build --to myproject`); + } + else { + console.log(`Example: ${scriptName} custom-command`); + } + process.exit(1); + } + runWithErrorAndStatusCode(logger, () => { + const version = _getRushVersion(logger); + logger.info(`The ${RUSH_JSON_FILENAME} configuration requests Rush version ${version}`); + const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE]; + if (lockFilePath) { + logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`); + } + return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath); + }); +} +_run(); +//# sourceMappingURL=install-run-rush.js.map +})(); + +module.exports = __webpack_exports__; +/******/ })() +; +//# sourceMappingURL=install-run-rush.js.map \ No newline at end of file diff --git a/foundations/core/common/scripts/install-run-rushx.js b/foundations/core/common/scripts/install-run-rushx.js new file mode 100644 index 0000000000..6581521f3c --- /dev/null +++ b/foundations/core/common/scripts/install-run-rushx.js @@ -0,0 +1,31 @@ +// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. +// +// This script is intended for usage in an automated build environment where the Rush command may not have +// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush +// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the +// rushx command. +// +// An example usage would be: +// +// node common/scripts/install-run-rushx.js custom-command +// +// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. + +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +var __webpack_exports__ = {}; +/*!*************************************************!*\ + !*** ./lib-esnext/scripts/install-run-rushx.js ***! + \*************************************************/ + +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +require('./install-run-rush'); +//# sourceMappingURL=install-run-rushx.js.map +module.exports = __webpack_exports__; +/******/ })() +; +//# sourceMappingURL=install-run-rushx.js.map \ No newline at end of file diff --git a/foundations/core/common/scripts/install-run.js b/foundations/core/common/scripts/install-run.js new file mode 100644 index 0000000000..a35726bf16 --- /dev/null +++ b/foundations/core/common/scripts/install-run.js @@ -0,0 +1,778 @@ +// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. +// +// This script is intended for usage in an automated build environment where a Node tool may not have +// been preinstalled, or may have an unpredictable version. This script will automatically install the specified +// version of the specified tool (if not already installed), and then pass a command-line to it. +// An example usage would be: +// +// node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io +// +// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. + +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ({ + +/***/ 16928: +/*!***********************!*\ + !*** external "path" ***! + \***********************/ +/***/ ((module) => { + +module.exports = require("path"); + +/***/ }), + +/***/ 179896: +/*!*********************!*\ + !*** external "fs" ***! + \*********************/ +/***/ ((module) => { + +module.exports = require("fs"); + +/***/ }), + +/***/ 370857: +/*!*********************!*\ + !*** external "os" ***! + \*********************/ +/***/ ((module) => { + +module.exports = require("os"); + +/***/ }), + +/***/ 535317: +/*!********************************!*\ + !*** external "child_process" ***! + \********************************/ +/***/ ((module) => { + +module.exports = require("child_process"); + +/***/ }), + +/***/ 832286: +/*!************************************************!*\ + !*** ./lib-esnext/utilities/npmrcUtilities.js ***! + \************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ isVariableSetInNpmrcFile: () => (/* binding */ isVariableSetInNpmrcFile), +/* harmony export */ syncNpmrc: () => (/* binding */ syncNpmrc), +/* harmony export */ trimNpmrcFileLines: () => (/* binding */ trimNpmrcFileLines) +/* harmony export */ }); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ 179896); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! path */ 16928); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +// IMPORTANT - do not use any non-built-in libraries in this file + + +/** + * This function reads the content for given .npmrc file path, and also trims + * unusable lines from the .npmrc file. + * + * @returns + * The text of the the .npmrc. + */ +// create a global _combinedNpmrc for cache purpose +const _combinedNpmrcMap = new Map(); +function _trimNpmrcFile(options) { + const { sourceNpmrcPath, linesToPrepend, linesToAppend, supportEnvVarFallbackSyntax } = options; + const combinedNpmrcFromCache = _combinedNpmrcMap.get(sourceNpmrcPath); + if (combinedNpmrcFromCache !== undefined) { + return combinedNpmrcFromCache; + } + let npmrcFileLines = []; + if (linesToPrepend) { + npmrcFileLines.push(...linesToPrepend); + } + if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { + npmrcFileLines.push(...fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\n')); + } + if (linesToAppend) { + npmrcFileLines.push(...linesToAppend); + } + npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim()); + const resultLines = trimNpmrcFileLines(npmrcFileLines, process.env, supportEnvVarFallbackSyntax); + const combinedNpmrc = resultLines.join('\n'); + //save the cache + _combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc); + return combinedNpmrc; +} +/** + * + * @param npmrcFileLines The npmrc file's lines + * @param env The environment variables object + * @param supportEnvVarFallbackSyntax Whether to support fallback values in the form of `${VAR_NAME:-fallback}` + * @returns + */ +function trimNpmrcFileLines(npmrcFileLines, env, supportEnvVarFallbackSyntax) { + var _a; + const resultLines = []; + // This finds environment variable tokens that look like "${VAR_NAME}" + const expansionRegExp = /\$\{([^\}]+)\}/g; + // Comment lines start with "#" or ";" + const commentRegExp = /^\s*[#;]/; + // Trim out lines that reference environment variables that aren't defined + for (let line of npmrcFileLines) { + let lineShouldBeTrimmed = false; + //remove spaces before or after key and value + line = line + .split('=') + .map((lineToTrim) => lineToTrim.trim()) + .join('='); + // Ignore comment lines + if (!commentRegExp.test(line)) { + const environmentVariables = line.match(expansionRegExp); + if (environmentVariables) { + for (const token of environmentVariables) { + /** + * Remove the leading "${" and the trailing "}" from the token + * + * ${nameString} -> nameString + * ${nameString-fallbackString} -> name-fallbackString + * ${nameString:-fallbackString} -> name:-fallbackString + */ + const nameWithFallback = token.substring(2, token.length - 1); + let environmentVariableName; + let fallback; + if (supportEnvVarFallbackSyntax) { + /** + * Get the environment variable name and fallback value. + * + * name fallback + * nameString -> nameString undefined + * nameString-fallbackString -> nameString fallbackString + * nameString:-fallbackString -> nameString fallbackString + */ + const matched = nameWithFallback.match(/^([^:-]+)(?:\:?-(.+))?$/); + // matched: [originStr, variableName, fallback] + environmentVariableName = (_a = matched === null || matched === void 0 ? void 0 : matched[1]) !== null && _a !== void 0 ? _a : nameWithFallback; + fallback = matched === null || matched === void 0 ? void 0 : matched[2]; + } + else { + environmentVariableName = nameWithFallback; + } + // Is the environment variable and fallback value defined. + if (!env[environmentVariableName] && !fallback) { + // No, so trim this line + lineShouldBeTrimmed = true; + break; + } + } + } + } + if (lineShouldBeTrimmed) { + // Example output: + // "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}" + resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line); + } + else { + resultLines.push(line); + } + } + return resultLines; +} +function _copyAndTrimNpmrcFile(options) { + const { logger, sourceNpmrcPath, targetNpmrcPath } = options; + logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose + logger.info(` --> "${targetNpmrcPath}"`); + const combinedNpmrc = _trimNpmrcFile(options); + fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(targetNpmrcPath, combinedNpmrc); + return combinedNpmrc; +} +function syncNpmrc(options) { + const { sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger = { + // eslint-disable-next-line no-console + info: console.log, + // eslint-disable-next-line no-console + error: console.error + }, createIfMissing = false } = options; + const sourceNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish'); + const targetNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(targetNpmrcFolder, '.npmrc'); + try { + if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath) || createIfMissing) { + // Ensure the target folder exists + if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcFolder)) { + fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(targetNpmrcFolder, { recursive: true }); + } + return _copyAndTrimNpmrcFile({ + sourceNpmrcPath, + targetNpmrcPath, + logger, + ...options + }); + } + else if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcPath)) { + // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target + logger.info(`Deleting ${targetNpmrcPath}`); // Verbose + fs__WEBPACK_IMPORTED_MODULE_0__.unlinkSync(targetNpmrcPath); + } + } + catch (e) { + throw new Error(`Error syncing .npmrc file: ${e}`); + } +} +function isVariableSetInNpmrcFile(sourceNpmrcFolder, variableKey, supportEnvVarFallbackSyntax) { + const sourceNpmrcPath = `${sourceNpmrcFolder}/.npmrc`; + //if .npmrc file does not exist, return false directly + if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { + return false; + } + const trimmedNpmrcFile = _trimNpmrcFile({ sourceNpmrcPath, supportEnvVarFallbackSyntax }); + const variableKeyRegExp = new RegExp(`^${variableKey}=`, 'm'); + return trimmedNpmrcFile.match(variableKeyRegExp) !== null; +} +//# sourceMappingURL=npmrcUtilities.js.map + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => (module['default']) : +/******/ () => (module); +/******/ __webpack_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. +(() => { +/*!*******************************************!*\ + !*** ./lib-esnext/scripts/install-run.js ***! + \*******************************************/ +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ RUSH_JSON_FILENAME: () => (/* binding */ RUSH_JSON_FILENAME), +/* harmony export */ findRushJsonFolder: () => (/* binding */ findRushJsonFolder), +/* harmony export */ getNpmPath: () => (/* binding */ getNpmPath), +/* harmony export */ installAndRun: () => (/* binding */ installAndRun), +/* harmony export */ runWithErrorAndStatusCode: () => (/* binding */ runWithErrorAndStatusCode) +/* harmony export */ }); +/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! child_process */ 535317); +/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(child_process__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! os */ 370857); +/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! path */ 16928); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); +/* harmony import */ var _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utilities/npmrcUtilities */ 832286); +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +/* eslint-disable no-console */ + + + + + +const RUSH_JSON_FILENAME = 'rush.json'; +const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER'; +const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH'; +const INSTALLED_FLAG_FILENAME = 'installed.flag'; +const NODE_MODULES_FOLDER_NAME = 'node_modules'; +const PACKAGE_JSON_FILENAME = 'package.json'; +/** + * Parse a package specifier (in the form of name\@version) into name and version parts. + */ +function _parsePackageSpecifier(rawPackageSpecifier) { + rawPackageSpecifier = (rawPackageSpecifier || '').trim(); + const separatorIndex = rawPackageSpecifier.lastIndexOf('@'); + let name; + let version = undefined; + if (separatorIndex === 0) { + // The specifier starts with a scope and doesn't have a version specified + name = rawPackageSpecifier; + } + else if (separatorIndex === -1) { + // The specifier doesn't have a version + name = rawPackageSpecifier; + } + else { + name = rawPackageSpecifier.substring(0, separatorIndex); + version = rawPackageSpecifier.substring(separatorIndex + 1); + } + if (!name) { + throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`); + } + return { name, version }; +} +let _npmPath = undefined; +/** + * Get the absolute path to the npm executable + */ +function getNpmPath() { + if (!_npmPath) { + try { + if (_isWindows()) { + // We're on Windows + const whereOutput = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('where npm', { stdio: [] }).toString(); + const lines = whereOutput.split(os__WEBPACK_IMPORTED_MODULE_2__.EOL).filter((line) => !!line); + // take the last result, we are looking for a .cmd command + // see https://github.com/microsoft/rushstack/issues/759 + _npmPath = lines[lines.length - 1]; + } + else { + // We aren't on Windows - assume we're on *NIX or Darwin + _npmPath = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('command -v npm', { stdio: [] }).toString(); + } + } + catch (e) { + throw new Error(`Unable to determine the path to the NPM tool: ${e}`); + } + _npmPath = _npmPath.trim(); + if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(_npmPath)) { + throw new Error('The NPM executable does not exist'); + } + } + return _npmPath; +} +function _ensureFolder(folderPath) { + if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(folderPath)) { + const parentDir = path__WEBPACK_IMPORTED_MODULE_3__.dirname(folderPath); + _ensureFolder(parentDir); + fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(folderPath); + } +} +/** + * Create missing directories under the specified base directory, and return the resolved directory. + * + * Does not support "." or ".." path segments. + * Assumes the baseFolder exists. + */ +function _ensureAndJoinPath(baseFolder, ...pathSegments) { + let joinedPath = baseFolder; + try { + for (let pathSegment of pathSegments) { + pathSegment = pathSegment.replace(/[\\\/]/g, '+'); + joinedPath = path__WEBPACK_IMPORTED_MODULE_3__.join(joinedPath, pathSegment); + if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(joinedPath)) { + fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(joinedPath); + } + } + } + catch (e) { + throw new Error(`Error building local installation folder (${path__WEBPACK_IMPORTED_MODULE_3__.join(baseFolder, ...pathSegments)}): ${e}`); + } + return joinedPath; +} +function _getRushTempFolder(rushCommonFolder) { + const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME]; + if (rushTempFolder !== undefined) { + _ensureFolder(rushTempFolder); + return rushTempFolder; + } + else { + return _ensureAndJoinPath(rushCommonFolder, 'temp'); + } +} +/** + * Compare version strings according to semantic versioning. + * Returns a positive integer if "a" is a later version than "b", + * a negative integer if "b" is later than "a", + * and 0 otherwise. + */ +function _compareVersionStrings(a, b) { + const aParts = a.split(/[.-]/); + const bParts = b.split(/[.-]/); + const numberOfParts = Math.max(aParts.length, bParts.length); + for (let i = 0; i < numberOfParts; i++) { + if (aParts[i] !== bParts[i]) { + return (Number(aParts[i]) || 0) - (Number(bParts[i]) || 0); + } + } + return 0; +} +/** + * Resolve a package specifier to a static version + */ +function _resolvePackageVersion(logger, rushCommonFolder, { name, version }) { + if (!version) { + version = '*'; // If no version is specified, use the latest version + } + if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) { + // If the version contains only characters that we recognize to be used in static version specifiers, + // pass the version through + return version; + } + else { + // version resolves to + try { + const rushTempFolder = _getRushTempFolder(rushCommonFolder); + const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); + (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ + sourceNpmrcFolder, + targetNpmrcFolder: rushTempFolder, + logger, + supportEnvVarFallbackSyntax: false + }); + const npmPath = getNpmPath(); + // This returns something that looks like: + // ``` + // [ + // "3.0.0", + // "3.0.1", + // ... + // "3.0.20" + // ] + // ``` + // + // if multiple versions match the selector, or + // + // ``` + // "3.0.0" + // ``` + // + // if only a single version matches. + const spawnSyncOptions = { + cwd: rushTempFolder, + stdio: [], + shell: _isWindows() + }; + const platformNpmPath = _getPlatformPath(npmPath); + const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'], spawnSyncOptions); + if (npmVersionSpawnResult.status !== 0) { + throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`); + } + const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString(); + const parsedVersionOutput = JSON.parse(npmViewVersionOutput); + const versions = Array.isArray(parsedVersionOutput) + ? parsedVersionOutput + : [parsedVersionOutput]; + let latestVersion = versions[0]; + for (let i = 1; i < versions.length; i++) { + const latestVersionCandidate = versions[i]; + if (_compareVersionStrings(latestVersionCandidate, latestVersion) > 0) { + latestVersion = latestVersionCandidate; + } + } + if (!latestVersion) { + throw new Error('No versions found for the specified version range.'); + } + return latestVersion; + } + catch (e) { + throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`); + } + } +} +let _rushJsonFolder; +/** + * Find the absolute path to the folder containing rush.json + */ +function findRushJsonFolder() { + if (!_rushJsonFolder) { + let basePath = __dirname; + let tempPath = __dirname; + do { + const testRushJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(basePath, RUSH_JSON_FILENAME); + if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(testRushJsonPath)) { + _rushJsonFolder = basePath; + break; + } + else { + basePath = tempPath; + } + } while (basePath !== (tempPath = path__WEBPACK_IMPORTED_MODULE_3__.dirname(basePath))); // Exit the loop when we hit the disk root + if (!_rushJsonFolder) { + throw new Error(`Unable to find ${RUSH_JSON_FILENAME}.`); + } + } + return _rushJsonFolder; +} +/** + * Detects if the package in the specified directory is installed + */ +function _isPackageAlreadyInstalled(packageInstallFolder) { + try { + const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); + if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(flagFilePath)) { + return false; + } + const fileContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(flagFilePath).toString(); + return fileContents.trim() === process.version; + } + catch (e) { + return false; + } +} +/** + * Delete a file. Fail silently if it does not exist. + */ +function _deleteFile(file) { + try { + fs__WEBPACK_IMPORTED_MODULE_1__.unlinkSync(file); + } + catch (err) { + if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') { + throw err; + } + } +} +/** + * Removes the following files and directories under the specified folder path: + * - installed.flag + * - + * - node_modules + */ +function _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) { + try { + const flagFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME); + _deleteFile(flagFile); + const packageLockFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, 'package-lock.json'); + if (lockFilePath) { + fs__WEBPACK_IMPORTED_MODULE_1__.copyFileSync(lockFilePath, packageLockFile); + } + else { + // Not running `npm ci`, so need to cleanup + _deleteFile(packageLockFile); + const nodeModulesFolder = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME); + if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(nodeModulesFolder)) { + const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler'); + fs__WEBPACK_IMPORTED_MODULE_1__.renameSync(nodeModulesFolder, path__WEBPACK_IMPORTED_MODULE_3__.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`)); + } + } + } + catch (e) { + throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`); + } +} +function _createPackageJson(packageInstallFolder, name, version) { + try { + const packageJsonContents = { + name: 'ci-rush', + version: '0.0.0', + dependencies: { + [name]: version + }, + description: "DON'T WARN", + repository: "DON'T WARN", + license: 'MIT' + }; + const packageJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, PACKAGE_JSON_FILENAME); + fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2)); + } + catch (e) { + throw new Error(`Unable to create package.json: ${e}`); + } +} +/** + * Run "npm install" in the package install folder. + */ +function _installPackage(logger, packageInstallFolder, name, version, command) { + try { + logger.info(`Installing ${name}...`); + const npmPath = getNpmPath(); + const platformNpmPath = _getPlatformPath(npmPath); + const result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, [command], { + stdio: 'inherit', + cwd: packageInstallFolder, + env: process.env, + shell: _isWindows() + }); + if (result.status !== 0) { + throw new Error(`"npm ${command}" encountered an error`); + } + logger.info(`Successfully installed ${name}@${version}`); + } + catch (e) { + throw new Error(`Unable to install package: ${e}`); + } +} +/** + * Get the ".bin" path for the package. + */ +function _getBinPath(packageInstallFolder, binName) { + const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); + const resolvedBinName = _isWindows() ? `${binName}.cmd` : binName; + return path__WEBPACK_IMPORTED_MODULE_3__.resolve(binFolderPath, resolvedBinName); +} +/** + * Returns a cross-platform path - windows must enclose any path containing spaces within double quotes. + */ +function _getPlatformPath(platformPath) { + return _isWindows() && platformPath.includes(' ') ? `"${platformPath}"` : platformPath; +} +function _isWindows() { + return os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32'; +} +/** + * Write a flag file to the package's install directory, signifying that the install was successful. + */ +function _writeFlagFile(packageInstallFolder) { + try { + const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); + fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(flagFilePath, process.version); + } + catch (e) { + throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`); + } +} +function installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs, lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE]) { + const rushJsonFolder = findRushJsonFolder(); + const rushCommonFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushJsonFolder, 'common'); + const rushTempFolder = _getRushTempFolder(rushCommonFolder); + const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`); + if (!_isPackageAlreadyInstalled(packageInstallFolder)) { + // The package isn't already installed + _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath); + const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); + (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ + sourceNpmrcFolder, + targetNpmrcFolder: packageInstallFolder, + logger, + supportEnvVarFallbackSyntax: false + }); + _createPackageJson(packageInstallFolder, packageName, packageVersion); + const command = lockFilePath ? 'ci' : 'install'; + _installPackage(logger, packageInstallFolder, packageName, packageVersion, command); + _writeFlagFile(packageInstallFolder); + } + const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`; + const statusMessageLine = new Array(statusMessage.length + 1).join('-'); + logger.info('\n' + statusMessage + '\n' + statusMessageLine + '\n'); + const binPath = _getBinPath(packageInstallFolder, packageBinName); + const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); + // Windows environment variables are case-insensitive. Instead of using SpawnSyncOptions.env, we need to + // assign via the process.env proxy to ensure that we append to the right PATH key. + const originalEnvPath = process.env.PATH || ''; + let result; + try { + // `npm` bin stubs on Windows are `.cmd` files + // Node.js will not directly invoke a `.cmd` file unless `shell` is set to `true` + const platformBinPath = _getPlatformPath(binPath); + process.env.PATH = [binFolderPath, originalEnvPath].join(path__WEBPACK_IMPORTED_MODULE_3__.delimiter); + result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformBinPath, packageBinArgs, { + stdio: 'inherit', + windowsVerbatimArguments: false, + shell: _isWindows(), + cwd: process.cwd(), + env: process.env + }); + } + finally { + process.env.PATH = originalEnvPath; + } + if (result.status !== null) { + return result.status; + } + else { + throw result.error || new Error('An unknown error occurred.'); + } +} +function runWithErrorAndStatusCode(logger, fn) { + process.exitCode = 1; + try { + const exitCode = fn(); + process.exitCode = exitCode; + } + catch (e) { + logger.error('\n\n' + e.toString() + '\n\n'); + } +} +function _run() { + const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, rawPackageSpecifier /* qrcode@^1.2.0 */, packageBinName /* qrcode */, ...packageBinArgs /* [-f, myproject/lib] */] = process.argv; + if (!nodePath) { + throw new Error('Unexpected exception: could not detect node path'); + } + if (path__WEBPACK_IMPORTED_MODULE_3__.basename(scriptPath).toLowerCase() !== 'install-run.js') { + // If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control + // to the script that (presumably) imported this file + return; + } + if (process.argv.length < 4) { + console.log('Usage: install-run.js @ [args...]'); + console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io'); + process.exit(1); + } + const logger = { info: console.log, error: console.error }; + runWithErrorAndStatusCode(logger, () => { + const rushJsonFolder = findRushJsonFolder(); + const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common'); + const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier); + const name = packageSpecifier.name; + const version = _resolvePackageVersion(logger, rushCommonFolder, packageSpecifier); + if (packageSpecifier.version !== version) { + console.log(`Resolved to ${name}@${version}`); + } + return installAndRun(logger, name, version, packageBinName, packageBinArgs); + }); +} +_run(); +//# sourceMappingURL=install-run.js.map +})(); + +module.exports = __webpack_exports__; +/******/ })() +; +//# sourceMappingURL=install-run.js.map \ No newline at end of file diff --git a/foundations/core/common/scripts/merge-coverage.js b/foundations/core/common/scripts/merge-coverage.js new file mode 100644 index 0000000000..7d55b07ce9 --- /dev/null +++ b/foundations/core/common/scripts/merge-coverage.js @@ -0,0 +1,106 @@ +#!/usr/bin/env node +const fs = require('fs') +const path = require('path') + +const root = process.cwd() +const patterns = ['packages', 'pods', 'tests'] +let files = [] +for (const p of patterns) { + const dir = path.join(root, p) + if (!fs.existsSync(dir)) continue + const items = fs.readdirSync(dir) + for (const it of items) { + const lcov = path.join(dir, it, 'coverage', 'lcov.info') + if (fs.existsSync(lcov)) files.push(lcov) + } +} + +if (files.length === 0) { + console.error('No lcov files found in packages/pods/tests/*/coverage/lcov.info') + process.exit(1) +} + +const outDir = path.join(root, 'coverage') +if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }) +const outFile = path.join(outDir, 'lcov.info') + +let outData = '' +let seenTN = false +// Build a repo file index to help resolve SF entries that are ambiguous +const ignoreDirs = new Set(['node_modules', '.git', 'coverage', 'lib', 'dist', 'types', '.rush', 'temp', 'pnpm-store']) +const repoFiles = [] +function walk(dir) { + const items = fs.readdirSync(dir, { withFileTypes: true }) + for (const it of items) { + if (it.isDirectory()) { + if (ignoreDirs.has(it.name)) continue + // skip hidden folders except top-level .config maybe + if (it.name.startsWith('.')) continue + try { + walk(path.join(dir, it.name)) + } catch (e) { + // ignore permission errors + } + } else if (it.isFile()) { + repoFiles.push(path.join(dir, it.name)) + } + } +} +try { + walk(root) +} catch (e) { + /* ignore */ +} + +for (const f of files) { + const data = fs.readFileSync(f, 'utf8') + const pkgDir = path.dirname(path.dirname(f)) + const lines = data.split(/\r?\n/) + const outLines = [] + for (const line of lines) { + if (!line) continue + // skip duplicate TN: headers (test name) + if (line.startsWith('TN:')) { + if (seenTN) continue + seenTN = true + outLines.push(line) + continue + } + + if (line.startsWith('SF:')) { + const orig = line.slice(3) + // if path is absolute and exists, keep it; otherwise resolve from package dir + if (path.isAbsolute(orig)) { + outLines.push('SF:' + orig) + continue + } + + const abs = path.resolve(pkgDir, orig) + if (fs.existsSync(abs)) { + outLines.push('SF:' + abs) + } else { + // try package/src/orig if orig is not already prefixed with src + const alt = path.resolve(pkgDir, orig) + if (fs.existsSync(alt)) { + outLines.push('SF:' + path.relative(root, alt)) + } else { + // try to find any file in repo that ends with the orig path + const found = repoFiles.find((p) => p.endsWith(path.sep + orig) || p.endsWith(orig)) + if (found) { + outLines.push('SF:' + found) + } else { + // keep original if we can't resolve + outLines.push('SF:' + orig) + } + } + } + continue + } + + outLines.push(line) + } + + outData += outLines.join('\n') + '\n' +} +fs.writeFileSync(outFile, outData, 'utf8') +console.log('Merged', files.length, 'lcov files into', outFile) diff --git a/foundations/core/common/scripts/package.json b/foundations/core/common/scripts/package.json new file mode 100644 index 0000000000..f4a987ee54 --- /dev/null +++ b/foundations/core/common/scripts/package.json @@ -0,0 +1,19 @@ +{ + "name": "@hcengineering/scripts", + "version": "0.7.17", + "scripts": { + "format": "echo \"No format specified\"" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-svelte": "^2.35.1" + }, + "private": true +} diff --git a/foundations/core/common/scripts/show-coverage.sh b/foundations/core/common/scripts/show-coverage.sh new file mode 100755 index 0000000000..cc99b27499 --- /dev/null +++ b/foundations/core/common/scripts/show-coverage.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +echo "=== FINAL COVERAGE REPORT ===" +echo "" + +# Iterate through each package directory +for pkg in packages/*/; do + pkgname=$(basename "$pkg") + + echo "📦 Package: $pkgname" + echo "---" + + # Change to package directory + cd "$pkg" || continue + + # Run tests with coverage and extract summary + npm test -- --coverage --silent 2>&1 | \ + grep -A 4 "Coverage summary" | \ + grep -E "Statements|Branches|Functions|Lines" + + # Return to root directory + cd ../.. || exit + + echo "" +done + +echo "=== END OF COVERAGE REPORT ===" \ No newline at end of file diff --git a/foundations/core/packages/account-client/.eslintrc.js b/foundations/core/packages/account-client/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/account-client/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/account-client/.npmignore b/foundations/core/packages/account-client/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/account-client/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/account-client/CHANGELOG.json b/foundations/core/packages/account-client/CHANGELOG.json new file mode 100644 index 0000000000..cef733b400 --- /dev/null +++ b/foundations/core/packages/account-client/CHANGELOG.json @@ -0,0 +1,132 @@ +{ + "name": "@hcengineering/account-client", + "entries": [ + { + "version": "0.7.19", + "tag": "@hcengineering/account-client_v0.7.19", + "date": "Thu, 30 Oct 2025 08:41:42 GMT", + "comments": { + "none": [ + { + "comment": "formatting" + } + ], + "patch": [ + { + "comment": "add workspace usage info" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.19` to `0.7.20`" + } + ] + } + }, + { + "version": "0.7.18", + "tag": "@hcengineering/account-client_v0.7.18", + "date": "Tue, 28 Oct 2025 21:50:57 GMT", + "comments": { + "patch": [ + { + "comment": "new sub methods" + } + ] + } + }, + { + "version": "0.7.17", + "tag": "@hcengineering/account-client_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.7", + "tag": "@hcengineering/account-client_v0.7.7", + "date": "Wed, 22 Oct 2025 12:46:09 GMT", + "comments": { + "patch": [ + { + "comment": "add subs methods" + } + ] + } + }, + { + "version": "0.7.6", + "tag": "@hcengineering/account-client_v0.7.6", + "date": "Wed, 15 Oct 2025 18:01:30 GMT", + "comments": { + "patch": [ + { + "comment": "added methods to work with profile" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.7` to `0.7.8`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/account-client_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/account-client_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/account-client_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/account-client/CHANGELOG.md b/foundations/core/packages/account-client/CHANGELOG.md new file mode 100644 index 0000000000..8640bd6fe0 --- /dev/null +++ b/foundations/core/packages/account-client/CHANGELOG.md @@ -0,0 +1,56 @@ +# Change Log - @hcengineering/account-client + +This log was last generated on Thu, 30 Oct 2025 08:41:42 GMT and should not be manually modified. + +## 0.7.19 +Thu, 30 Oct 2025 08:41:42 GMT + +### Patches + +- add workspace usage info + +## 0.7.18 +Tue, 28 Oct 2025 21:50:57 GMT + +### Patches + +- new sub methods + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.7 +Wed, 22 Oct 2025 12:46:09 GMT + +### Patches + +- add subs methods + +## 0.7.6 +Wed, 15 Oct 2025 18:01:30 GMT + +### Patches + +- added methods to work with profile + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/account-client/config/rig.json b/foundations/core/packages/account-client/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/account-client/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/account-client/jest.config.js b/foundations/core/packages/account-client/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/account-client/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/account-client/package.json b/foundations/core/packages/account-client/package.json new file mode 100644 index 0000000000..f2d75506b5 --- /dev/null +++ b/foundations/core/packages/account-client/package.json @@ -0,0 +1,60 @@ +{ + "name": "@hcengineering/account-client", + "version": "0.7.19", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent --coverage", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "cross-env": "~7.0.3", + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@types/node": "^22.18.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "esbuild": "^0.25.10", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/platform": "workspace:^0.7.18" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/account-client/src/client.ts b/foundations/core/packages/account-client/src/client.ts new file mode 100644 index 0000000000..92b7c5f7a5 --- /dev/null +++ b/foundations/core/packages/account-client/src/client.ts @@ -0,0 +1,1277 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { + type AccountInfo, + type AccountRole, + type AccountUuid, + type BackupStatus, + concatLink, + Data, + type Person, + type PersonId, + type PersonInfo, + type PersonUuid, + type SocialIdType, + Version, + type UsageStatus, + type WorkspaceInfoWithStatus, + type WorkspaceMemberInfo, + WorkspaceMode, + type WorkspaceUserOperation, + type WorkspaceUuid +} from '@hcengineering/core' +import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' +import type { + AccountAggregatedInfo, + Integration, + IntegrationKey, + IntegrationSecret, + IntegrationSecretKey, + LoginInfo, + LoginInfoByToken, + LoginInfoRequestData, + LoginInfoWithWorkspaces, + MailboxInfo, + MailboxOptions, + MailboxSecret, + OtpInfo, + PersonWithProfile, + ProviderInfo, + RegionInfo, + SocialId, + Subscription, + SubscriptionData, + UserProfile, + WorkspaceLoginInfo, + WorkspaceOperation +} from './types' +import { getClientTimezone } from './utils' + +/** @public */ +export interface AccountClient { + // Static methods + getProviders: () => Promise + + // RPC + getUserWorkspaces: () => Promise + selectWorkspace: ( + workspaceUrl: string, + kind?: 'external' | 'internal' | 'byregion', + externalRegions?: string[] + ) => Promise + validateOtp: (email: string, code: string, password?: string, action?: 'verify') => Promise + loginOtp: (email: string) => Promise + getLoginInfoByToken: (data?: LoginInfoRequestData) => Promise + getLoginWithWorkspaceInfo: () => Promise + restorePassword: (password: string) => Promise + confirm: () => Promise + requestPasswordReset: (email: string) => Promise + sendInvite: (email: string, role: AccountRole) => Promise + resendInvite: (email: string, role: AccountRole) => Promise + createInviteLink: ( + email: string, + role: AccountRole, + autoJoin: boolean, + firstName: string, + lastName: string, + navigateUrl?: string, + expHours?: number + ) => Promise + leaveWorkspace: (account: AccountUuid) => Promise + changeUsername: (first: string, last: string) => Promise + changePassword: (oldPassword: string, newPassword: string) => Promise + signUpJoin: ( + email: string, + password: string, + first: string, + last: string, + inviteId: string, + workspaceUrl: string + ) => Promise + join: (email: string, password: string, inviteId: string, workspaceUrl: string) => Promise + createInvite: (exp: number, emailMask: string, limit: number, role: AccountRole) => Promise + /** + * @param options.personalized + * If true, will generate a link with a personalized token for one person access + * If false, will generate a link with an open-ended account in the token. Every token use will generate a new account. + * When false, notBefore and expiration parameters are mandatory. + * @param options.notBefore - not valid before; timestamp in seconds + * @param options.expiration - expires after; timestamp in seconds + */ + createAccessLink: ( + role: AccountRole, + options?: { + firstName?: string + lastName?: string + navigateUrl?: string + extra?: Record + spaces?: string[] + notBefore?: number + expiration?: number + personalized?: boolean + } + ) => Promise + checkJoin: (inviteId: string) => Promise + checkAutoJoin: (inviteId: string, firstName?: string, lastName?: string) => Promise + getWorkspaceInfo: (updateLastVisit?: boolean) => Promise + getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise + updateLastVisit: (workspaces: WorkspaceUuid[]) => Promise + getRegionInfo: () => Promise + createWorkspace: (name: string, region?: string) => Promise + signUpOtp: (email: string, first: string, last: string) => Promise + /** + * Deprecated. Only to be used for dev setups without mail service. + */ + signUp: (email: string, password: string, first: string, last: string) => Promise + login: (email: string, password: string) => Promise + loginAsGuest: () => Promise + isReadOnlyGuest: () => Promise + getPerson: () => Promise + getPersonInfo: (account: PersonUuid) => Promise + getSocialIds: (includeDeleted?: boolean) => Promise + getWorkspaceMembers: () => Promise + updateWorkspaceRole: (account: string, role: AccountRole) => Promise + updateAllowReadOnlyGuests: ( + readOnlyGuestsAllowed: boolean + ) => Promise<{ guestPerson: Person, guestSocialIds: SocialId[] } | undefined> + updateAllowGuestSignUp: (guestSignUpAllowed: boolean) => Promise + updateWorkspaceName: (name: string) => Promise + deleteWorkspace: () => Promise + findPersonBySocialKey: (socialKey: string, requireAccount?: boolean) => Promise + findPersonBySocialId: (socialId: PersonId, requireAccount?: boolean) => Promise + findSocialIdBySocialKey: (socialKey: string) => Promise + findFullSocialIdBySocialKey: (socialKey: string) => Promise + findFullSocialIds: (socialIds: PersonId[]) => Promise + getMailboxOptions: () => Promise + getMailboxSecret: (mailbox: string) => Promise + createMailbox: (name: string, domain: string) => Promise<{ mailbox: string, socialId: PersonId }> + getMailboxes: () => Promise + deleteMailbox: (mailbox: string) => Promise + listAccounts: (search?: string, skip?: number, limit?: number) => Promise + deleteAccount: (uuid: AccountUuid) => Promise + + workerHandshake: (region: string, version: Data, operation: WorkspaceOperation) => Promise + getPendingWorkspace: ( + region: string, + version: Data, + operation: WorkspaceOperation + ) => Promise + updateWorkspaceInfo: ( + wsUuid: string, + event: string, + version: Data, + progress: number, + message?: string + ) => Promise + listWorkspaces: (region?: string | null, mode?: WorkspaceMode | null) => Promise + performWorkspaceOperation: ( + workspaceId: string | string[], + event: WorkspaceUserOperation, + ...params: any + ) => Promise + assignWorkspace: (email: string, workspaceUuid: string, role: AccountRole) => Promise + updateBackupInfo: (info: BackupStatus) => Promise + updateUsageInfo: (info: UsageStatus) => Promise + updateWorkspaceRoleBySocialKey: (socialKey: string, targetRole: AccountRole) => Promise + ensurePerson: ( + socialType: SocialIdType, + socialValue: string, + firstName: string, + lastName: string + ) => Promise<{ uuid: PersonUuid, socialId: PersonId }> + addSocialIdToPerson: ( + person: PersonUuid, + type: SocialIdType, + value: string, + confirmed: boolean, + displayValue?: string + ) => Promise + updateSocialId: (personId: PersonId, displayValue: string) => Promise + exchangeGuestToken: (token: string) => Promise + /** + * Releases the target social id for the target account. + * If called with user's token it releases the social id for the user's account. + * @param personUuid Required for services + * @param type Social id type + * @param value Social id value + * @param deleteIntegrations Deletes associated integrations if true. Otherwise, throws an error if any. + * @returns Deleted social id with updated isDeleted flag and key/value + */ + releaseSocialId: ( + personUuid: PersonUuid | undefined, + type: SocialIdType, + value: string, + deleteIntegrations?: boolean + ) => Promise + createIntegration: (integration: Integration) => Promise + updateIntegration: (integration: Integration) => Promise + deleteIntegration: (integrationKey: IntegrationKey) => Promise + getIntegration: (integrationKey: IntegrationKey) => Promise + listIntegrations: (filter: Partial) => Promise + addIntegrationSecret: (integrationSecret: IntegrationSecret) => Promise + updateIntegrationSecret: (integrationSecret: IntegrationSecret) => Promise + deleteIntegrationSecret: (integrationSecretKey: IntegrationSecretKey) => Promise + getIntegrationSecret: (integrationSecretKey: IntegrationSecretKey) => Promise + listIntegrationsSecrets: (filter: Partial) => Promise + getAccountInfo: (uuid: AccountUuid) => Promise + canMergeSpecifiedPersons: (primaryPerson: PersonUuid, secondaryPerson: PersonUuid) => Promise + mergeSpecifiedPersons: (primaryPerson: PersonUuid, secondaryPerson: PersonUuid) => Promise + mergeSpecifiedAccounts: (primaryAccount: AccountUuid, secondaryAccount: AccountUuid) => Promise + addEmailSocialId: (email: string) => Promise + addHulyAssistantSocialId: () => Promise + refreshHulyAssistantToken: () => Promise + + setMyProfile: (profile: Partial>) => Promise + getUserProfile: (personUuid?: PersonUuid) => Promise + + getSubscriptions: (workspaceUuid?: WorkspaceUuid | undefined, activeOnly?: boolean) => Promise + getSubscriptionByProviderId: (provider: string, providerSubscriptionId: string) => Promise + getSubscriptionById: (subscriptionId: string) => Promise + upsertSubscription: (subscription: SubscriptionData) => Promise + + setCookie: () => Promise + deleteCookie: () => Promise +} + +/** @public */ +export function getClient (accountsUrl?: string, token?: string, retryTimeoutMs?: number): AccountClient { + if (accountsUrl === undefined) { + throw new Error('Accounts url not specified') + } + + return new AccountClientImpl(accountsUrl, token, retryTimeoutMs) +} + +interface Request { + method: string + params: Record +} + +class AccountClientImpl implements AccountClient { + private readonly request: RequestInit + private readonly rpc: typeof this._rpc + + constructor ( + private readonly url: string, + private readonly token?: string, + retryTimeoutMs?: number + ) { + if (url === '') { + throw new Error('Accounts url not specified') + } + + const isBrowser = typeof window !== 'undefined' + + this.request = { + keepalive: true, + headers: { + ...(this.token === undefined + ? {} + : { + Authorization: 'Bearer ' + this.token + }) + }, + ...(isBrowser ? { credentials: 'include' } : {}) + } + this.rpc = withRetryUntilTimeout(this._rpc.bind(this), retryTimeoutMs ?? 5000) + } + + async getProviders (): Promise { + return await withRetryUntilMaxAttempts(async () => { + const response = await fetch(concatLink(this.url, '/providers')) + + return await response.json() + })() + } + + private async _rpc(request: Request): Promise { + const timezone = getClientTimezone() + const meta: Record = timezone !== undefined ? { 'x-timezone': timezone } : {} + const response = await fetch(this.url, { + ...this.request, + headers: { + ...this.request.headers, + 'Content-Type': 'application/json', + Connection: 'keep-alive', + ...meta + }, + method: 'POST', + body: JSON.stringify(request) + }) + + const result = await response.json() + if (result.error != null) { + throw new PlatformError(result.error) + } + + return result.result + } + + private flattenStatus (ws: any): WorkspaceInfoWithStatus { + if (ws === undefined) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, {})) + } + + const status = ws.status + if (status === undefined) { + return ws + } + + const result = { ...ws, ...status, processingAttemps: status.processingAttempts ?? 0 } + delete result.status + + return result + } + + async getUserWorkspaces (): Promise { + const request = { + method: 'getUserWorkspaces' as const, + params: {} + } + + return (await this.rpc(request)).map((ws) => this.flattenStatus(ws)) + } + + async selectWorkspace ( + workspaceUrl: string, + kind: 'external' | 'internal' | 'byregion' = 'external', + externalRegions: string[] = [] + ): Promise { + const request = { + method: 'selectWorkspace' as const, + params: { workspaceUrl, kind, externalRegions } + } + + return await this.rpc(request) + } + + async validateOtp (email: string, code: string, password?: string, action?: 'verify'): Promise { + const request = { + method: 'validateOtp' as const, + params: { email, code, password, action } + } + + return await this.rpc(request) + } + + async loginOtp (email: string): Promise { + const request = { + method: 'loginOtp' as const, + params: { email } + } + + return await this.rpc(request) + } + + async getLoginInfoByToken (data?: LoginInfoRequestData): Promise { + const request = { + method: 'getLoginInfoByToken' as const, + params: data ?? {} + } + + return await this.rpc(request) + } + + async getLoginWithWorkspaceInfo (): Promise { + const request = { + method: 'getLoginWithWorkspaceInfo' as const, + params: {} + } + + return await this.rpc(request) + } + + async restorePassword (password: string): Promise { + const request = { + method: 'restorePassword' as const, + params: { password } + } + + return await this.rpc(request) + } + + async confirm (): Promise { + const request = { + method: 'confirm' as const, + params: {} + } + + return await this.rpc(request) + } + + async requestPasswordReset (email: string): Promise { + const request = { + method: 'requestPasswordReset' as const, + params: { email } + } + + await this.rpc(request) + } + + async sendInvite (email: string, role: AccountRole): Promise { + const request = { + method: 'sendInvite' as const, + params: { email, role } + } + + await this.rpc(request) + } + + async resendInvite (email: string, role: AccountRole): Promise { + const request = { + method: 'resendInvite' as const, + params: { email, role } + } + + await this.rpc(request) + } + + async createInviteLink ( + email: string, + role: AccountRole, + autoJoin: boolean, + firstName: string, + lastName: string, + navigateUrl?: string, + expHours?: number + ): Promise { + const request = { + method: 'createInviteLink' as const, + params: { email, role, autoJoin, firstName, lastName, navigateUrl, expHours } + } + + return await this.rpc(request) + } + + async createAccessLink ( + role: AccountRole, + options?: { + firstName?: string + lastName?: string + navigateUrl?: string + extra?: Record + spaces?: string[] + notBefore?: number + expiration?: number + personalized?: boolean + } + ): Promise { + const params: any = { ...(options ?? {}), role } + if (params.extra != null) { + params.extra = JSON.stringify(params.extra) + } + + const request = { + method: 'createAccessLink' as const, + params + } + + return await this.rpc(request) + } + + async leaveWorkspace (account: AccountUuid): Promise { + const request = { + method: 'leaveWorkspace' as const, + params: { account } + } + + return await this.rpc(request) + } + + async changeUsername (first: string, last: string): Promise { + const request = { + method: 'changeUsername' as const, + params: { first, last } + } + + await this.rpc(request) + } + + async changePassword (oldPassword: string, newPassword: string): Promise { + const request = { + method: 'changePassword' as const, + params: { oldPassword, newPassword } + } + + await this.rpc(request) + } + + async signUpJoin ( + email: string, + password: string, + first: string, + last: string, + inviteId: string, + workspaceUrl: string + ): Promise { + const request = { + method: 'signUpJoin' as const, + params: { email, password, first, last, inviteId, workspaceUrl } + } + + return await this.rpc(request) + } + + async join (email: string, password: string, inviteId: string, workspaceUrl: string): Promise { + const request = { + method: 'join' as const, + params: { email, password, inviteId, workspaceUrl } + } + + return await this.rpc(request) + } + + async createInvite (exp: number, emailMask: string, limit: number, role: AccountRole): Promise { + const request = { + method: 'createInvite' as const, + params: { exp, emailMask, limit, role } + } + + return await this.rpc(request) + } + + async checkJoin (inviteId: string): Promise { + const request = { + method: 'checkJoin' as const, + params: { inviteId } + } + + return await this.rpc(request) + } + + async checkAutoJoin (inviteId: string, firstName?: string, lastName?: string): Promise { + const request = { + method: 'checkAutoJoin' as const, + params: { inviteId, firstName, lastName } + } + + return await this.rpc(request) + } + + async getWorkspacesInfo (ids: WorkspaceUuid[]): Promise { + const request = { + method: 'getWorkspacesInfo' as const, + params: { ids } + } + const infos: any[] = await this.rpc(request) + return Array.from(infos).map((it) => this.flattenStatus(it)) + } + + async updateLastVisit (ids: WorkspaceUuid[]): Promise { + const request = { + method: 'updateLastVisit' as const, + params: { ids } + } + await this.rpc(request) + } + + async getWorkspaceInfo (updateLastVisit: boolean = false): Promise { + const request = { + method: 'getWorkspaceInfo' as const, + params: updateLastVisit ? { updateLastVisit: true } : {} + } + + return this.flattenStatus(await this.rpc(request)) + } + + async getRegionInfo (): Promise { + const request = { + method: 'getRegionInfo' as const, + params: {} + } + + return await this.rpc(request) + } + + async createWorkspace (workspaceName: string, region?: string): Promise { + const request = { + method: 'createWorkspace' as const, + params: { workspaceName, region } + } + + return await this.rpc(request) + } + + async signUpOtp (email: string, firstName: string, lastName: string): Promise { + const request = { + method: 'signUpOtp' as const, + params: { email, firstName, lastName } + } + + return await this.rpc(request) + } + + async signUp (email: string, password: string, firstName: string, lastName: string): Promise { + const request = { + method: 'signUp' as const, + params: { email, password, firstName, lastName } + } + + return await this.rpc(request) + } + + async login (email: string, password: string): Promise { + const request = { + method: 'login' as const, + params: { email, password } + } + + return await this.rpc(request) + } + + async loginAsGuest (): Promise { + const request = { + method: 'loginAsGuest' as const, + params: {} + } + + return await this.rpc(request) + } + + async isReadOnlyGuest (): Promise { + const request = { + method: 'isReadOnlyGuest' as const, + params: {} + } + + return await this.rpc(request) + } + + async getPerson (): Promise { + const request = { + method: 'getPerson' as const, + params: {} + } + + return await this.rpc(request) + } + + async getPersonInfo (account: PersonUuid): Promise { + const request = { + method: 'getPersonInfo' as const, + params: { account } + } + + return await this.rpc(request) + } + + async getSocialIds (includeDeleted?: boolean): Promise { + const request = { + method: 'getSocialIds' as const, + params: { includeDeleted } + } + + return await this.rpc(request) + } + + async workerHandshake (region: string, version: Data, operation: WorkspaceOperation): Promise { + const request = { + method: 'workerHandshake' as const, + params: { region, version, operation } + } + + await this.rpc(request) + } + + async getPendingWorkspace ( + region: string, + version: Data, + operation: WorkspaceOperation + ): Promise { + const request = { + method: 'getPendingWorkspace' as const, + params: { region, version, operation } + } + + const result = await this.rpc(request) + if (result == null) { + return null + } + + return this.flattenStatus(result) + } + + async updateWorkspaceInfo ( + workspaceUuid: string, + event: string, + version: Data, + progress: number, + message?: string + ): Promise { + const request = { + method: 'updateWorkspaceInfo' as const, + params: { workspaceUuid, event, version, progress, message } + } + + await this.rpc(request) + } + + async updateAllowReadOnlyGuests ( + readOnlyGuestsAllowed: boolean + ): Promise<{ guestPerson: Person, guestSocialIds: SocialId[] } | undefined> { + const request = { + method: 'updateAllowReadOnlyGuests' as const, + params: { readOnlyGuestsAllowed } + } + + return await this.rpc(request) + } + + async updateAllowGuestSignUp (guestSignUpAllowed: boolean): Promise { + const request = { + method: 'updateAllowGuestSignUp' as const, + params: { guestSignUpAllowed } + } + + await this.rpc(request) + } + + async getWorkspaceMembers (): Promise { + const request = { + method: 'getWorkspaceMembers' as const, + params: {} + } + + return await this.rpc(request) + } + + async updateWorkspaceRole (targetAccount: string, targetRole: AccountRole): Promise { + const request = { + method: 'updateWorkspaceRole' as const, + params: { targetAccount, targetRole } + } + + await this.rpc(request) + } + + async updateWorkspaceName (name: string): Promise { + const request = { + method: 'updateWorkspaceName' as const, + params: { name } + } + + await this.rpc(request) + } + + async deleteWorkspace (): Promise { + const request = { + method: 'deleteWorkspace' as const, + params: {} + } + + await this.rpc(request) + } + + async findPersonBySocialKey (socialString: string, requireAccount?: boolean): Promise { + const request = { + method: 'findPersonBySocialKey' as const, + params: { socialString, requireAccount } + } + + return await this.rpc(request) + } + + async findPersonBySocialId (socialId: PersonId, requireAccount?: boolean): Promise { + const request = { + method: 'findPersonBySocialId' as const, + params: { socialId, requireAccount } + } + + return await this.rpc(request) + } + + async findSocialIdBySocialKey (socialKey: string): Promise { + const request = { + method: 'findSocialIdBySocialKey' as const, + params: { socialKey } + } + + return await this.rpc(request) + } + + async findFullSocialIdBySocialKey (socialKey: string): Promise { + const request = { + method: 'findFullSocialIdBySocialKey' as const, + params: { socialKey } + } + return await this.rpc(request) + } + + async findFullSocialIds (socialIds: PersonId[]): Promise { + const request = { + method: 'findFullSocialIds' as const, + params: { socialIds } + } + return await this.rpc(request) + } + + async listWorkspaces (region?: string | null, mode: WorkspaceMode | null = null): Promise { + const request = { + method: 'listWorkspaces' as const, + params: { region, mode } + } + + return ((await this.rpc(request)) ?? []).map((ws) => this.flattenStatus(ws)) + } + + async performWorkspaceOperation ( + workspaceId: string | string[], + event: WorkspaceUserOperation, + ...params: any + ): Promise { + const request = { + method: 'performWorkspaceOperation' as const, + params: { workspaceId, event, params } + } + + return await this.rpc(request) + } + + async updateBackupInfo (backupInfo: BackupStatus): Promise { + const request = { + method: 'updateBackupInfo' as const, + params: { backupInfo } + } + + await this.rpc(request) + } + + async updateUsageInfo (usageInfo: UsageStatus): Promise { + const request = { + method: 'updateUsageInfo' as const, + params: { usageInfo } + } + + await this.rpc(request) + } + + async assignWorkspace (email: string, workspaceUuid: string, role: AccountRole): Promise { + const request = { + method: 'assignWorkspace' as const, + params: { email, workspaceUuid, role } + } + + await this.rpc(request) + } + + async updateWorkspaceRoleBySocialKey (socialKey: string, targetRole: AccountRole): Promise { + const request = { + method: 'updateWorkspaceRoleBySocialKey' as const, + params: { socialKey, targetRole } + } + + await this.rpc(request) + } + + async ensurePerson ( + socialType: SocialIdType, + socialValue: string, + firstName: string, + lastName: string + ): Promise<{ uuid: PersonUuid, socialId: PersonId }> { + const request = { + method: 'ensurePerson' as const, + params: { socialType, socialValue, firstName, lastName } + } + + return await this.rpc(request) + } + + async exchangeGuestToken (token: string): Promise { + const request = { + method: 'exchangeGuestToken' as const, + params: { token } + } + + return await this.rpc(request) + } + + async addSocialIdToPerson ( + person: PersonUuid, + type: SocialIdType, + value: string, + confirmed: boolean, + displayValue?: string + ): Promise { + const request = { + method: 'addSocialIdToPerson' as const, + params: { person, type, value, confirmed, displayValue } + } + + return await this.rpc(request) + } + + async updateSocialId (personId: PersonId, displayValue: string): Promise { + const request = { + method: 'updateSocialId' as const, + params: { personId, displayValue } + } + return await this.rpc(request) + } + + async getMailboxOptions (): Promise { + const request = { + method: 'getMailboxOptions' as const, + params: {} + } + + return await this.rpc(request) + } + + async getMailboxSecret (mailbox: string): Promise { + const request = { + method: 'getMailboxSecret' as const, + params: { mailbox } + } + + return await this.rpc(request) + } + + async createMailbox (name: string, domain: string): Promise<{ mailbox: string, socialId: PersonId }> { + const request = { + method: 'createMailbox' as const, + params: { name, domain } + } + + return await this.rpc(request) + } + + async getMailboxes (): Promise { + const request = { + method: 'getMailboxes' as const, + params: {} + } + + return await this.rpc(request) + } + + async deleteMailbox (mailbox: string): Promise { + const request = { + method: 'deleteMailbox' as const, + params: { mailbox } + } + + await this.rpc(request) + } + + async listAccounts (search?: string, skip?: number, limit?: number): Promise { + const request = { + method: 'listAccounts' as const, + params: { search, skip, limit } + } + + return await this.rpc(request) + } + + async deleteAccount (uuid: AccountUuid): Promise { + const request = { + method: 'deleteAccount' as const, + params: { uuid } + } + + await this.rpc(request) + } + + async releaseSocialId ( + personUuid: PersonUuid | undefined, + type: SocialIdType, + value: string, + deleteIntegrations = false + ): Promise { + const request = { + method: 'releaseSocialId' as const, + params: { personUuid, type, value, deleteIntegrations } + } + + return await this.rpc(request) + } + + async createIntegration (integration: Integration): Promise { + const request = { + method: 'createIntegration' as const, + params: integration + } + + await this.rpc(request) + } + + async updateIntegration (integration: Integration): Promise { + const request = { + method: 'updateIntegration' as const, + params: integration + } + + await this.rpc(request) + } + + async deleteIntegration (integrationKey: IntegrationKey): Promise { + const request = { + method: 'deleteIntegration' as const, + params: integrationKey + } + + await this.rpc(request) + } + + async getIntegration (integrationKey: IntegrationKey): Promise { + const request = { + method: 'getIntegration' as const, + params: integrationKey + } + + return await this.rpc(request) + } + + async listIntegrations (filter: Partial): Promise { + const request = { + method: 'listIntegrations' as const, + params: filter + } + + return await this.rpc(request) + } + + async addIntegrationSecret (integrationSecret: IntegrationSecret): Promise { + const request = { + method: 'addIntegrationSecret' as const, + params: integrationSecret + } + + await this.rpc(request) + } + + async updateIntegrationSecret (integrationSecret: IntegrationSecret): Promise { + const request = { + method: 'updateIntegrationSecret' as const, + params: integrationSecret + } + + await this.rpc(request) + } + + async deleteIntegrationSecret (integrationSecretKey: IntegrationSecretKey): Promise { + const request = { + method: 'deleteIntegrationSecret' as const, + params: integrationSecretKey + } + + await this.rpc(request) + } + + async getIntegrationSecret (integrationSecretKey: IntegrationSecretKey): Promise { + const request = { + method: 'getIntegrationSecret' as const, + params: integrationSecretKey + } + + return await this.rpc(request) + } + + async listIntegrationsSecrets (filter: Partial): Promise { + const request = { + method: 'listIntegrationsSecrets' as const, + params: filter + } + + return await this.rpc(request) + } + + async getAccountInfo (uuid: AccountUuid): Promise { + const request = { + method: 'getAccountInfo' as const, + params: { accountId: uuid } + } + + return await this.rpc(request) + } + + async canMergeSpecifiedPersons (primaryPerson: PersonUuid, secondaryPerson: PersonUuid): Promise { + const request = { + method: 'canMergeSpecifiedPersons' as const, + params: { primaryPerson, secondaryPerson } + } + + return await this.rpc(request) + } + + async mergeSpecifiedPersons (primaryPerson: PersonUuid, secondaryPerson: PersonUuid): Promise { + const request = { + method: 'mergeSpecifiedPersons' as const, + params: { primaryPerson, secondaryPerson } + } + + await this.rpc(request) + } + + async mergeSpecifiedAccounts (primaryAccount: AccountUuid, secondaryAccount: AccountUuid): Promise { + const request = { + method: 'mergeSpecifiedAccounts' as const, + params: { primaryAccount, secondaryAccount } + } + + await this.rpc(request) + } + + async addEmailSocialId (email: string): Promise { + const request = { + method: 'addEmailSocialId' as const, + params: { email } + } + + return await this.rpc(request) + } + + async addHulyAssistantSocialId (): Promise { + const request = { + method: 'addHulyAssistantSocialId' as const, + params: {} + } + + return await this.rpc(request) + } + + async refreshHulyAssistantToken (): Promise { + const request = { + method: 'refreshHulyAssistantToken' as const, + params: {} + } + + await this.rpc(request) + } + + async setCookie (): Promise { + const url = concatLink(this.url, '/cookie') + const response = await fetch(url, { ...this.request, method: 'PUT' }) + + if (!response.ok) { + const result = await response.json() + if (result.error != null) { + throw new PlatformError(result.error) + } + } + } + + async deleteCookie (): Promise { + const url = concatLink(this.url, '/cookie') + const response = await fetch(url, { ...this.request, method: 'DELETE' }) + + if (!response.ok) { + const result = await response.json() + if (result.error != null) { + throw new PlatformError(result.error) + } + } + } + + async setMyProfile (profile: Partial>): Promise { + const request = { + method: 'setMyProfile', + params: { + profile + } + } + + await this._rpc(request) + } + + async getUserProfile (personUuid?: PersonUuid): Promise { + return await this._rpc({ + method: 'getUserProfile', + params: { + personUuid + } + }) + } + + async getSubscriptions ( + workspaceUuid: WorkspaceUuid | undefined = undefined, + activeOnly: boolean = true + ): Promise { + return await this._rpc({ + method: 'getSubscriptions', + params: { + workspaceUuid, + activeOnly + } + }) + } + + async getSubscriptionByProviderId (provider: string, providerSubscriptionId: string): Promise { + return await this._rpc({ + method: 'getSubscriptionByProviderId', + params: { + provider, + providerSubscriptionId + } + }) + } + + async getSubscriptionById (subscriptionId: string): Promise { + return await this._rpc({ + method: 'getSubscriptionById', + params: { + subscriptionId + } + }) + } + + async upsertSubscription (subscription: SubscriptionData): Promise { + await this._rpc({ + method: 'upsertSubscription', + params: subscription + }) + } +} + +function withRetry Promise> ( + f: F, + shouldFail: (err: any, attempt: number) => boolean, + intervalMs: number = 25 +): F { + return async function (...params: any[]): Promise { + let attempt = 0 + while (true) { + try { + return await f(...params) + } catch (err: any) { + if (shouldFail(err, attempt)) { + throw err + } + + attempt++ + await new Promise((resolve) => setTimeout(resolve, intervalMs)) + if (intervalMs < 1000) { + intervalMs += 100 + } + } + } + } as F +} + +const connectionErrorCodes = ['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND'] + +function withRetryUntilTimeout Promise> (f: F, timeoutMs: number = 5000): F { + const timeout = Date.now() + timeoutMs + const shouldFail = (err: any): boolean => !connectionErrorCodes.includes(err?.cause?.code) || timeout < Date.now() + + return withRetry(f, shouldFail) +} + +function withRetryUntilMaxAttempts Promise> (f: F, maxAttempts: number = 5): F { + const shouldFail = (err: any, attempt: number): boolean => + !connectionErrorCodes.includes(err?.cause?.code) || attempt === maxAttempts + + return withRetry(f, shouldFail) +} diff --git a/foundations/core/packages/account-client/src/index.ts b/foundations/core/packages/account-client/src/index.ts new file mode 100644 index 0000000000..b487cf8270 --- /dev/null +++ b/foundations/core/packages/account-client/src/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './client' +export * from './types' +export * from './utils' diff --git a/foundations/core/packages/account-client/src/types.ts b/foundations/core/packages/account-client/src/types.ts new file mode 100644 index 0000000000..748d35196d --- /dev/null +++ b/foundations/core/packages/account-client/src/types.ts @@ -0,0 +1,235 @@ +import { + type AccountUuid, + PersonId, + WorkspaceDataId, + WorkspaceUuid, + type AccountRole, + type Timestamp, + type SocialId as SocialIdBase, + PersonUuid, + type WorkspaceMode, + Person, + WorkspaceInfo, + AccountInfo, + IntegrationKind +} from '@hcengineering/core' + +export interface LoginInfo { + account: AccountUuid + name?: string + socialId?: PersonId + token?: string +} + +export interface EndpointInfo { + internalUrl: string + externalUrl: string + region: string +} +export interface WorkspaceVersion { + versionMajor: number + versionMinor: number + versionPatch: number +} + +export interface LoginInfoWorkspace { + url: string + dataId?: WorkspaceDataId + mode: WorkspaceMode + version: WorkspaceVersion + endpoint: EndpointInfo + role: AccountRole | null + progress?: number + branding?: string +} + +export interface LoginInfoWithWorkspaces extends LoginInfo { + // Information necessary to handle user <--> transactor connectivity. + workspaces: Record + socialIds: SocialId[] +} + +export type LoginInfoByToken = LoginInfo | WorkspaceLoginInfo | LoginInfoRequest | null + +/** + * @public + */ +export interface WorkspaceLoginInfo extends LoginInfo { + workspace: WorkspaceUuid // worspace uuid + workspaceDataId?: WorkspaceDataId + workspaceUrl: string + endpoint: string + token: string + role: AccountRole + allowGuestSignUp?: boolean +} + +export interface LoginInfoRequestData { + firstName?: string + lastName?: string +} + +export type LoginInfoRequest = { + request: true +} & LoginInfoRequestData + +export interface WorkspaceInviteInfo { + workspace: WorkspaceUuid + email?: string + name?: string +} + +export interface OtpInfo { + sent: boolean + retryOn: Timestamp +} + +export interface RegionInfo { + region: string + name: string +} + +export type WorkspaceOperation = 'create' | 'upgrade' | 'all' | 'all+backup' + +export interface MailboxOptions { + availableDomains: string[] + minNameLength: number + maxNameLength: number + maxMailboxCount: number +} + +export interface MailboxInfo { + mailbox: string + aliases: string[] + appPasswords: string[] +} + +export interface MailboxSecret { + mailbox: string + app?: string + secret: string +} + +export interface Integration { + socialId: PersonId + kind: IntegrationKind // Integration kind. E.g. 'github', 'mail', 'telegram-bot', 'telegram' etc. + workspaceUuid: WorkspaceUuid | null + data?: Record + disabled?: boolean +} + +export interface SocialId extends SocialIdBase { + personUuid: PersonUuid + isDeleted?: boolean +} + +export type IntegrationKey = Omit + +export interface IntegrationSecret { + socialId: PersonId + kind: IntegrationKind // Integration kind. E.g. 'github', 'mail', 'telegram-bot', 'telegram' etc. + workspaceUuid: WorkspaceUuid | null + key: string // Key for the secret in the integration. Different secrets for the same integration must have different keys. Can be any string. E.g. '', 'user_app_1' etc. + secret: string +} + +export type IntegrationSecretKey = Omit + +export interface ProviderInfo { + name: string + displayName?: string +} + +export interface AccountAggregatedInfo extends AccountInfo, Person { + uuid: AccountUuid + integrations: Omit[] + socialIds: SocialId[] + workspaces: Omit[] +} + +/** + * User profile with additional information for public sharing + * Stored in accounts database (global, not workspace-specific) + */ +export interface UserProfile { + personUuid: PersonUuid + bio?: string // LinkedIn-style bio (up to ~2000 chars) + city?: string + country?: string + website?: string // Personal website URL + socialLinks?: Record // Flexible storage for social links + isPublic: boolean // Public visibility toggle (default: false) +} + +export type PersonWithProfile = Person & Omit + +/** + * Subscription status enum + * Reflects the subscription lifecycle from active to canceled/expired + */ +export enum SubscriptionStatus { + Active = 'active', // Subscription is active and paid + Trialing = 'trialing', // In trial period (free usage) + PastDue = 'past_due', // Payment failed but subscription not yet canceled + Canceled = 'canceled', // Subscription was canceled by user or admin + Paused = 'paused', // Subscription is temporarily paused (some providers support this) + Expired = 'expired' // Subscription or trial has expired +} + +/** + * Subscription type/purpose + * Allows multiple active subscriptions per workspace for different purposes + */ +export enum SubscriptionType { + Tier = 'tier', // Main workspace tier (free, starter, pro, enterprise) + Support = 'support' // Voluntary support/donation subscription +} + +/** + * Workspace subscription information + * Provider-agnostic subscription data managed by billing service + * Multiple subscriptions can be active per workspace (tier + addons + support) + * Historical subscriptions are preserved with status: canceled/expired + */ +export interface Subscription { + id: string // Our internal unique subscription ID (UUID) + workspaceUuid: WorkspaceUuid + accountUuid: AccountUuid // Account that paid for the subscription + + // Provider details + provider: string // Payment provider identifier (e.g. 'polar', 'stripe', 'manual') + providerSubscriptionId: string // External subscription ID from the provider + providerCheckoutId?: string // External checkout/session ID that created this subscription + + // Subscription classification + type: SubscriptionType // What this subscription is for (tier, addon, support) + status: SubscriptionStatus // Current status + plan: string // Plan/product identifier (e.g. 'free', 'pro', 'storage-100gb', 'supporter') + + // Amount paid (in cents, e.g. 9999 = $99.99) + // Used primarily for pay-what-you-want/donation subscriptions to track actual payment + amount?: number + + // Billing period (optional - not set for free/manual plans) + periodStart?: Timestamp + periodEnd?: Timestamp + + // Trial information (optional) + trialEnd?: Timestamp + + // Cancellation tracking (optional) + canceledAt?: Timestamp + + // Provider-specific data stored as JSONB (optional) + providerData?: Record + + // Timestamps (managed by database) + createdOn: Timestamp + updatedOn: Timestamp +} + +/** + * Subscription data for creating/updating subscriptions (without timestamps) + * Used by billing service to upsert subscription data + */ +export type SubscriptionData = Omit diff --git a/foundations/core/packages/account-client/src/utils.ts b/foundations/core/packages/account-client/src/utils.ts new file mode 100644 index 0000000000..a8afa6a817 --- /dev/null +++ b/foundations/core/packages/account-client/src/utils.ts @@ -0,0 +1,33 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { LoginInfoByToken, LoginInfoRequest, WorkspaceLoginInfo } from './types' + +export function isWorkspaceLoginInfo (loginInfo: LoginInfoByToken): loginInfo is WorkspaceLoginInfo { + return !isLoginInfoRequest(loginInfo) && (loginInfo as WorkspaceLoginInfo)?.workspace != null +} + +export function isLoginInfoRequest (info: LoginInfoByToken): info is LoginInfoRequest { + return (info as LoginInfoRequest)?.request +} + +export function getClientTimezone (): string | undefined { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone + } catch (err: any) { + console.error('Failed to get client timezone', err) + return undefined + } +} diff --git a/foundations/core/packages/account-client/tsconfig.json b/foundations/core/packages/account-client/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/account-client/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/analytics-service/.eslintrc.js b/foundations/core/packages/analytics-service/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/analytics-service/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/analytics-service/.npmignore b/foundations/core/packages/analytics-service/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/analytics-service/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/analytics-service/CHANGELOG.json b/foundations/core/packages/analytics-service/CHANGELOG.json new file mode 100644 index 0000000000..347b4621cf --- /dev/null +++ b/foundations/core/packages/analytics-service/CHANGELOG.json @@ -0,0 +1,75 @@ +{ + "name": "@hcengineering/analytics-service", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/analytics-service_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/analytics-service_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + }, + { + "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/analytics-service_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/analytics-service_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/analytics-service/CHANGELOG.md b/foundations/core/packages/analytics-service/CHANGELOG.md new file mode 100644 index 0000000000..044186394a --- /dev/null +++ b/foundations/core/packages/analytics-service/CHANGELOG.md @@ -0,0 +1,28 @@ +# Change Log - @hcengineering/analytics-service + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/analytics-service/config/rig.json b/foundations/core/packages/analytics-service/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/analytics-service/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/analytics-service/jest.config.js b/foundations/core/packages/analytics-service/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/analytics-service/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/analytics-service/package.json b/foundations/core/packages/analytics-service/package.json new file mode 100644 index 0000000000..4070788497 --- /dev/null +++ b/foundations/core/packages/analytics-service/package.json @@ -0,0 +1,61 @@ +{ + "name": "@hcengineering/analytics-service", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/platform": "workspace:^0.7.18", + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/analytics": "workspace:^0.7.17", + "@hcengineering/measurements-otlp": "workspace:^0.7.17", + "winston": "^3.11.0", + "winston-daily-rotate-file": "^5.0.0" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/analytics-service/src/index.ts b/foundations/core/packages/analytics-service/src/index.ts new file mode 100644 index 0000000000..3f7c9a2ff6 --- /dev/null +++ b/foundations/core/packages/analytics-service/src/index.ts @@ -0,0 +1,42 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import { AnalyticProvider, Analytics } from '@hcengineering/analytics' +import { initOpenTelemetrySDK, reportOTELError } from '@hcengineering/measurements-otlp' + +export * from '@hcengineering/measurements-otlp' +export * from './logging' + +class OTELAnalyticsProvider implements AnalyticProvider { + init (config: Record): boolean { + return true + } + + setUser: (email: string, data: any) => void = (email, data) => {} + + setAlias: (distinctId: string, alias: string) => void = (distinctId, alias) => {} + + setTag: (key: string, value: string) => void = (key, value) => {} + + setWorkspace: (ws: string, guest: boolean) => void = (ws, guest) => {} + + handleEvent: (event: string, params: Record) => void = (event, params) => {} + + handleError (error: Error): void { + reportOTELError(error) + } + + navigate (path: string): void {} + + logout (): void {} +} + +export function configureAnalytics (serviceName: string, serviceVersion: string, config?: Record): void { + const providers: AnalyticProvider[] = [new OTELAnalyticsProvider()] + + initOpenTelemetrySDK(serviceName, serviceVersion) + for (const provider of providers) { + Analytics.init(provider, config ?? {}) + } +} diff --git a/foundations/core/packages/analytics-service/src/logging.ts b/foundations/core/packages/analytics-service/src/logging.ts new file mode 100644 index 0000000000..2ccf0db338 --- /dev/null +++ b/foundations/core/packages/analytics-service/src/logging.ts @@ -0,0 +1,121 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +import { MeasureLogger, ParamsType } from '@hcengineering/core' +import { basename, dirname, join } from 'path' +import winston from 'winston' +import DailyRotateFile from 'winston-daily-rotate-file' + +export class SplitLogger implements MeasureLogger { + logger: winston.Logger + + constructor ( + readonly name: string, + readonly opts: { root?: string, parent?: winston.Logger, pretty?: boolean, enableConsole?: boolean } + ) { + const rootDir = this.opts.root ?? 'logs' + + this.logger = winston.createLogger({ + level: 'info', + exitOnError: false + }) + const errorPrinter = ({ message, stack, ...rest }: Error): object => ({ + message, + stack, + ...rest + }) + const jsonOptions: winston.Logform.JsonOptions = { + replacer: (key, value) => { + return value instanceof Error ? errorPrinter(value) : value + } + } + this.logger.add( + new DailyRotateFile({ + format: winston.format.combine( + winston.format.timestamp(), + opts.pretty === true ? winston.format.prettyPrint() : winston.format.json(jsonOptions) + ), + filename: `${name}-combined-%DATE%.log`, + auditFile: join(rootDir, `${basename(name)}-audit.log`), + dirname: rootDir, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d' + }) + ) + this.logger.add( + new DailyRotateFile({ + format: winston.format.combine(winston.format.timestamp(), winston.format.prettyPrint()), + filename: `${name}-error-%DATE%.log`, + auditFile: join(rootDir, `${basename(name)}-audit.log`), + level: 'error', + dirname: rootDir, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d' + }) + ) + if (opts.parent === undefined && opts.enableConsole === true) { + console.log('Logging also into console', process.env.NODE_ENV, opts.enableConsole) + this.logger.add( + new winston.transports.Console({ + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json(jsonOptions), + winston.format.colorize({ all: true }) + ) + }) + ) + } + this.logger.info( + '####################################################################################################################' + ) + this.logger.info( + `########################SplitLogger ${this.name} initialized: ${new Date().toISOString()}###########################` + ) + } + + error (message: string, obj?: Record): void { + if (this.opts.parent !== undefined) { + this.opts.parent.error({ message, ...obj }) + } + this.logger.error({ message, ...obj }) + } + + info (message: string, obj?: Record): void { + if (this.opts.parent !== undefined && this.opts.enableConsole === true) { + // Only propogate if enable console is true + this.opts.parent.info({ message, ...obj }) + } + this.logger.info({ message, ...obj }) + } + + warn (message: string, obj?: Record): void { + if (this.opts.parent !== undefined) { + this.opts.parent.warn({ message, ...obj }) + } + this.logger.warn({ message, ...obj }) + } + + logOperation (operation: string, time: number, params: ParamsType): void { + this.logger.info(operation, { time, ...params }) + } + + childLogger (name: string, params: Record): MeasureLogger { + const dirName = dirname(name) + const { enableConsole, ...otherParams } = params + const child = this.logger.child({ name, ...otherParams }) + return new SplitLogger(name, { + ...this.opts, + parent: child, + root: join(this.opts.root ?? 'logs', dirName), + enableConsole: enableConsole === 'true' + }) + } + + async close (): Promise { + this.logger.close() + } +} diff --git a/foundations/core/packages/analytics-service/tsconfig.json b/foundations/core/packages/analytics-service/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/analytics-service/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/analytics/.eslintrc.js b/foundations/core/packages/analytics/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/analytics/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/analytics/.npmignore b/foundations/core/packages/analytics/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/analytics/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/analytics/CHANGELOG.json b/foundations/core/packages/analytics/CHANGELOG.json new file mode 100644 index 0000000000..5714d257f5 --- /dev/null +++ b/foundations/core/packages/analytics/CHANGELOG.json @@ -0,0 +1,51 @@ +{ + "name": "@hcengineering/analytics", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/analytics_v0.7.17", + "date": "Fri, 31 Oct 2025 20:11:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/analytics_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/analytics_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/analytics/CHANGELOG.md b/foundations/core/packages/analytics/CHANGELOG.md new file mode 100644 index 0000000000..12e6001636 --- /dev/null +++ b/foundations/core/packages/analytics/CHANGELOG.md @@ -0,0 +1,23 @@ +# Change Log - @hcengineering/analytics + +This log was last generated on Fri, 31 Oct 2025 20:11:13 GMT and should not be manually modified. + +## 0.7.17 +Fri, 31 Oct 2025 20:11:13 GMT + +_Version update only_ + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + diff --git a/foundations/core/packages/analytics/config/rig.json b/foundations/core/packages/analytics/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/analytics/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/analytics/jest.config.js b/foundations/core/packages/analytics/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/analytics/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/analytics/package.json b/foundations/core/packages/analytics/package.json new file mode 100644 index 0000000000..bb78bafcb7 --- /dev/null +++ b/foundations/core/packages/analytics/package.json @@ -0,0 +1,56 @@ +{ + "name": "@hcengineering/analytics", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/platform": "workspace:^0.7.18" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/analytics/src/index.ts b/foundations/core/packages/analytics/src/index.ts new file mode 100644 index 0000000000..31f28b4a02 --- /dev/null +++ b/foundations/core/packages/analytics/src/index.ts @@ -0,0 +1,84 @@ +// +// Copyright © 2024 Hardcore Engineering Inc +// + +import { addEventListener, PlatformEvent, Severity, Status, translate } from '@hcengineering/platform' + +export const providers: AnalyticProvider[] = [] +export interface AnalyticProvider { + init: (config: Record) => boolean + setUser: (email: string, data: any) => void + setAlias: (distinctId: string, alias: string) => void + setTag: (key: string, value: string) => void + setWorkspace: (ws: string, guest: boolean) => void + handleEvent: (event: string, params: Record) => void + handleError: (error: Error) => void + navigate: (path: string) => void + logout: () => void +} + +export const Analytics = { + data: {}, + + init (provider: AnalyticProvider, config: Record): void { + const res = provider.init(config) + if (res) { + providers.push(provider) + } + }, + + setUser (email: string, data: any): void { + providers.forEach((provider) => { + provider.setUser(email, data) + }) + }, + + setAlias (distinctId: string, alias: string): void { + providers.forEach((provider) => { + provider.setAlias(distinctId, alias) + }) + }, + + setTag (key: string, value: string): void { + providers.forEach((provider) => { + provider.setTag(key, value) + }) + }, + + setWorkspace (ws: string, guest: boolean): void { + providers.forEach((provider) => { + provider.setWorkspace(ws, guest) + }) + }, + + handleEvent (event: string, params: Record = {}): void { + providers.forEach((provider) => { + provider.handleEvent(event, { ...this.data, ...params }) + }) + }, + + handleError (error: Error): void { + providers.forEach((provider) => { + provider.handleError(error) + }) + }, + + navigate (path: string): void { + providers.forEach((provider) => { + provider.navigate(path) + }) + }, + + logout (): void { + providers.forEach((provider) => { + provider.logout() + }) + } +} + +addEventListener(PlatformEvent, async (_event, _status: Status) => { + if (_status.severity === Severity.ERROR) { + const label = await translate(_status.code, _status.params, 'en') + Analytics.handleError(new Error(label)) + } +}) diff --git a/foundations/core/packages/analytics/tsconfig.json b/foundations/core/packages/analytics/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/analytics/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/api-client/.eslintrc.js b/foundations/core/packages/api-client/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/api-client/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/api-client/.npmignore b/foundations/core/packages/api-client/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/api-client/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/api-client/CHANGELOG.json b/foundations/core/packages/api-client/CHANGELOG.json new file mode 100644 index 0000000000..1f381bf589 --- /dev/null +++ b/foundations/core/packages/api-client/CHANGELOG.json @@ -0,0 +1,122 @@ +{ + "name": "@hcengineering/api-client", + "entries": [ + { + "version": "0.7.18", + "tag": "@hcengineering/api-client_v0.7.18", + "date": "Mon, 27 Oct 2025 17:09:21 GMT", + "comments": { + "patch": [ + { + "comment": "add support for textColor and textStyle marks" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.17", + "tag": "@hcengineering/api-client_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/api-client_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/account-client\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/client\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/client-resources\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/collaborator-client\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/text-markdown\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/api-client_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/account-client\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/client\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/client-resources\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/collaborator-client\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/text-markdown\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/api-client_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/api-client/CHANGELOG.md b/foundations/core/packages/api-client/CHANGELOG.md new file mode 100644 index 0000000000..5698adeb18 --- /dev/null +++ b/foundations/core/packages/api-client/CHANGELOG.md @@ -0,0 +1,35 @@ +# Change Log - @hcengineering/api-client + +This log was last generated on Mon, 27 Oct 2025 17:09:21 GMT and should not be manually modified. + +## 0.7.18 +Mon, 27 Oct 2025 17:09:21 GMT + +### Patches + +- add support for textColor and textStyle marks + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/api-client/README.md b/foundations/core/packages/api-client/README.md new file mode 100644 index 0000000000..eea04475dd --- /dev/null +++ b/foundations/core/packages/api-client/README.md @@ -0,0 +1,446 @@ +# Huly Platform API Client + +A TypeScript client library for interacting with the Huly Platform API. + +## Installation + +In order to be able to install required packages, you will need to obtain GitHub access token. You can create a token by following the instructions [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-with-a-personal-access-token). + +```bash +npm install @hcengineering/api-client +``` + +## WebSocket Client vs REST Client + +The api client package provides two main client variants: a WebSocket client and a REST client. The WebSocket client holds persistent connection to the Huly Platform API. The REST client uses standard HTTP requests to perform operations. + +### WebSocket Client + +```ts +import { connect } from '@hcengineering/api-client' + +// Connect to Huly +const client = await connect('https://huly.app', { + email: 'johndoe@example.com', + password: 'password', + workspace: 'my-workspace', +}) + +// Use the client to perform operations +... + +// Close the client when done +await client.close() +``` + +### REST Client + +```ts +import { connectRest } from '@hcengineering/api-client' + +// Connect to Huly +const client = await connectRest('https://huly.app', { + email: 'johndoe@example.com', + password: 'password', + workspace: 'my-workspace' +}) + +// Use the client to perform operations +... + +``` + +## Authentication + +The client supports two authentication methods: using email and password, or using a token. +When authenticated, the client will have access to the same resources as the user. + +> Note: The examples below use the WebSocket client (`connect`). To use the REST client instead, import and call `connectRest` with the same options. + +Parameters: + +- `url`: URL of the Huly instance, for Huly Cloud use `https://huly.app` +- `options`: Connection options + - `workspace`: Name of the workspace to connect to, the workspace name can be found in the URL of the workspace: `https://huly.app/workbench/` + - `token`: Optional authentication token + - `email`: Optional user email + - `password`: Optional user password + +### Using Email and Password + +```ts +import { connect } from '@hcengineering/api-client' + +const client = await connect('https://huly.app', { + email: 'johndoe@example.com', + password: 'password', + workspace: 'my-workspace' +}) + +... + +await client.close() +``` + +### Using Token + +```ts +import { connect } from '@hcengineering/api-client' + +const client = await connect('https://huly.app', { + token: '...', + workspace: 'my-workspace' +}) + +... + +await client.close() +``` + +## Client API + +The client provides a set of methods for interacting with the Huly Platform API. This section describes the main methods available in the client. + +### Fetch API + +The client provides two main methods for retrieving documents: `findOne` and `findAll`. + +#### findOne + +Retrieves a single document matching the query criteria. + +Parameters: + +- `_class`: Class of the object to find, results will include all subclasses of the target class +- `query`: Query criteria +- `options`: Find options + - `limit`: Limit the number of results returned + - `sort`: Sorting criteria + - `lookup`: Lookup criteria + - `projection`: Projection criteria + - `total`: If specified total will be returned + +Example: + +```ts +import contact from '@hcengineering/contact' + +... + +const person = await client.findOne( + contact.class.Person, + { + _id: 'person-id' + } +) +``` + +#### findAll + +Retrieves multiple document matching the query criteria. + +Parameters: + +- `_class`: Class of the object to find, results will include all subclasses of the target class +- `query`: Query criteria +- `options`: Find options + - `limit`: Limit the number of results returned + - `sort`: Sorting criteria + - `lookup`: Lookup criteria + - `projection`: Projection criteria + - `total`: If specified total will be returned + +Example: + +```ts +import { SortingOrder } from '@hcengineering/core' +import contact from '@hcengineering/contact' + +.. + +const persons = await client.findAll( + contact.class.Person, + { + city: 'New York' + }, + { + limit: 10, + sort: { + name: SortingOrder.Ascending + } + } +) +``` + +### Documents API + +The client provides three main methods for managing documents: `createDoc`, `updateDoc`, and `removeDoc`. These methods allow you to perform CRUD operations on documents. + +#### createDoc + +Creates a new document in the specified space. + +Parameters: + +- `_class`: Class of the object +- `space`: Space of the object +- `attributes`: Attributes of the object +- `id`: Optional id of the object, if not provided, a new id will be generated + +Example: + +```ts +import contact, { AvatarType } from '@hcengineering/contact' + +.. + +const personId = await client.createDoc( + contact.class.Person, + contact.space.Contacts, + { + name: 'Doe,John', + city: 'New York', + avatarType: AvatarType.COLOR + } +) +``` + +#### updateDoc + +Updates existing document. + +Parameters: + +- `_class`: Class of the object +- `space`: Space of the object +- `objectId`: Id of the object +- `operations`: Attributes of the object to update + +Example: + +```ts +import contact from '@hcengineering/contact' + +.. + +await client.updateDoc( + contact.class.Person, + contact.space.Contacts, + personId, + { + city: 'New York', + } +) +``` + +#### removeDoc + +Removes existing document. + +Parameters: + +- `_class`: Class of the object +- `space`: Space of the object +- `objectId`: Id of the object + +Example: + +```ts +import contact from '@hcengineering/contact' + +.. + +await client.removeDoc( + contact.class.Person, + contact.space.Contacts, + personId +) +``` + +### Collections API + +#### addCollection + +Creates a new attached document in the specified collection. + +Parameters: + +- `_class`: Class of the object to create +- `space`: Space of the object to create +- `attachedTo`: Id of the object to attach to +- `attachedToClass`: Class of the object to attach to +- `collection`: Name of the collection containing attached documents +- `attributes`: Attributes of the object +- `id`: Optional id of the object, if not provided, a new id will be generated + +Example: + +```ts +import contact, { AvatarType } from '@hcengineering/contact' + +.. + +const personId = await client.createDoc( + contact.class.Person, + contact.space.Contacts, + { + name: 'Doe,John', + city: 'New York', + avatarType: AvatarType.COLOR + } +) + +await client.addCollection( + contact.class.Channel, + contact.space.Contacts, + personId, + contact.class.Person, + 'channels', + { + provider: contact.channelProvider.Email, + value: 'john.doe@example.com' + } +) +``` + +#### updateCollection + +Updates existing attached document in collection. + +Parameters: + +- `_class`: Class of the object to update +- `space`: Space of the object to update +- `objectId`: Space of the object to update +- `attachedTo`: Id of the parent object +- `attachedToClass`: Class of the parent object +- `collection`: Name of the collection containing attached documents +- `attributes`: Attributes of the object to update + +Example: + +```ts +import contact from '@hcengineering/contact' + +.. + +await client.updateCollection( + contact.class.Channel, + contact.space.Contacts, + channelId, + personId, + contact.class.Person, + 'channels', + { + city: 'New York', + } +) +``` + +#### removeCollection + +Removes existing attached document from collection. + +Parameters: + +- `_class`: Class of the object to remove +- `space`: Space of the object to remove +- `objectId`: Space of the object to remove +- `attachedTo`: Id of the parent object +- `attachedToClass`: Class of the parent object +- `collection`: Name of the collection containing attached documents + +Example: + +```ts +import contact from '@hcengineering/contact' + +.. + +await client.removeCollection( + contact.class.Channel, + contact.space.Contacts, + channelId, + personId, + contact.class.Person, + 'channels' +) +``` + +### Mixins API + +The client provides two methods for managing mixins: `createMixin` and `updateMixin`. + +#### createMixin + +Creates a new mixin for a specified document. + +Parameters: + +- `objectId`: Id of the object the mixin is attached to +- `objectClass`: Class of the object the mixin is attached to +- `objectSpace`: Space of the object the mixin is attached to +- `mixin`: Id of the mixin type to update +- `attributes`: Attributes of the mixin + +```ts +import contact, { AvatarType } from '@hcengineering/contact' + +.. + +const personId = await client.createDoc( + contact.class.Person, + contact.space.Contacts, + { + name: 'Doe,John', + city: 'New York', + avatarType: AvatarType.COLOR + } +) + +await client.createMixin( + personId, + contact.class.Person, + contact.space.Contacts, + contact.mixin.Employee, + { + active: true, + position: 'CEO' + } +) +``` + +#### updateMixin + +Updates an existing mixin. + +Parameters: + +- `objectId`: Id of the object the mixin is attached to +- `objectClass`: Class of the object the mixin is attached to +- `objectSpace`: Space of the object the mixin is attached to +- `mixin`: Id of the mixin type to update +- `attributes`: Attributes of the mixin to update + +```ts +import contact, { AvatarType } from '@hcengineering/contact' + +.. + +const person = await client.findOne( + contact.class.Person, + { + _id: 'person-id' + } +) + +await client.updateMixin( + personId, + contact.class.Person, + contact.space.Contacts, + contact.mixin.Employee, + { + active: false + } +) +``` diff --git a/foundations/core/packages/api-client/config/rig.json b/foundations/core/packages/api-client/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/api-client/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/api-client/jest.config.js b/foundations/core/packages/api-client/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/api-client/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/api-client/package.json b/foundations/core/packages/api-client/package.json new file mode 100644 index 0000000000..15d69d48f1 --- /dev/null +++ b/foundations/core/packages/api-client/package.json @@ -0,0 +1,72 @@ +{ + "name": "@hcengineering/api-client", + "version": "0.7.18", + "main": "lib/index.js", + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + }, + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "template": "@hcengineering/api-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", + "@types/node": "^22.18.1", + "@types/jest": "^29.5.5", + "@types/ws": "^8.5.12", + "@types/snappyjs": "^0.7.1", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/account-client": "workspace:^0.7.19", + "@hcengineering/client": "workspace:^0.7.17", + "@hcengineering/client-resources": "workspace:^0.7.17", + "@hcengineering/collaborator-client": "workspace:^0.7.17", + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/platform": "workspace:^0.7.18", + "@hcengineering/text": "workspace:^0.7.18", + "@hcengineering/text-markdown": "workspace:^0.7.20", + "snappyjs": "^0.7.0" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "optionalDependencies": { + "ws": "^8.18.2" + } +} diff --git a/foundations/core/packages/api-client/src/__tests__/config.test.ts b/foundations/core/packages/api-client/src/__tests__/config.test.ts new file mode 100644 index 0000000000..7a44d62887 --- /dev/null +++ b/foundations/core/packages/api-client/src/__tests__/config.test.ts @@ -0,0 +1,78 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// + +import { loadServerConfig } from '../config' + +describe('loadServerConfig', () => { + const mockFetch = jest.fn() + global.fetch = mockFetch as any + + beforeEach(() => { + mockFetch.mockClear() + }) + + it('should load server config successfully', async () => { + const mockConfig = { + ACCOUNTS_URL: 'https://accounts.example.com', + COLLABORATOR_URL: 'https://collaborator.example.com', + FILES_URL: 'https://files.example.com', + UPLOAD_URL: 'https://upload.example.com' + } + + mockFetch.mockResolvedValue({ + ok: true, + json: async () => mockConfig + }) + + const config = await loadServerConfig('https://api.example.com') + + expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/config.json', { keepalive: true }) + expect(config).toEqual(mockConfig) + }) + + it('should throw error when fetch fails', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 404 + }) + + await expect(loadServerConfig('https://api.example.com')).rejects.toThrow('Failed to fetch config') + }) + + it('should handle network errors', async () => { + mockFetch.mockRejectedValue(new Error('Network error')) + + await expect(loadServerConfig('https://api.example.com')).rejects.toThrow('Network error') + }) + + it('should construct correct config URL', async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ + ACCOUNTS_URL: '', + COLLABORATOR_URL: '', + FILES_URL: '', + UPLOAD_URL: '' + }) + }) + + await loadServerConfig('https://api.example.com/') + expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/config.json', { keepalive: true }) + }) + + it('should handle URL without trailing slash', async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ + ACCOUNTS_URL: '', + COLLABORATOR_URL: '', + FILES_URL: '', + UPLOAD_URL: '' + }) + }) + + await loadServerConfig('https://api.example.com') + expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/config.json', { keepalive: true }) + }) +}) diff --git a/foundations/core/packages/api-client/src/__tests__/markup-client.test.ts b/foundations/core/packages/api-client/src/__tests__/markup-client.test.ts new file mode 100644 index 0000000000..ece7fffa76 --- /dev/null +++ b/foundations/core/packages/api-client/src/__tests__/markup-client.test.ts @@ -0,0 +1,183 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// + +import { createMarkupOperations } from '../markup/client' +import { getClient } from '@hcengineering/collaborator-client' +import { makeCollabId } from '@hcengineering/core' + +// Mock dependencies +jest.mock('@hcengineering/collaborator-client') +jest.mock('@hcengineering/text', () => ({ + htmlToJSON: jest.fn((html) => ({ type: 'doc', content: [{ type: 'text', text: html }] })), + jsonToHTML: jest.fn((json) => json.content?.[0]?.text ?? ''), + jsonToMarkup: jest.fn((json) => json.content?.[0]?.text ?? ''), + markupToJSON: jest.fn((markup) => ({ type: 'doc', content: [{ type: 'text', text: markup }] })) +})) +jest.mock('@hcengineering/text-markdown', () => ({ + markdownToMarkup: jest.fn((md) => md), + markupToMarkdown: jest.fn((json) => json.content?.[0]?.text ?? '') +})) + +describe('MarkupOperations', () => { + const mockConfig = { + ACCOUNTS_URL: 'https://accounts.example.com', + COLLABORATOR_URL: 'https://collaborator.example.com', + FILES_URL: 'https://files.example.com', + UPLOAD_URL: 'https://upload.example.com' + } + + const workspace = 'test-workspace' as any + const token = 'test-token' + const url = 'https://api.example.com' + + let mockCollaborator: any + let operations: any + + beforeEach(() => { + jest.clearAllMocks() + + mockCollaborator = { + getMarkup: jest.fn(), + createMarkup: jest.fn() + } + ;(getClient as jest.Mock).mockReturnValue(mockCollaborator) + + operations = createMarkupOperations(url, workspace, token, mockConfig) + }) + + describe('fetchMarkup', () => { + const objectClass = 'class:test.Doc' as any + const objectId = 'doc-id-123' as any + const objectAttr = 'content' + const markupRef = 'markup-ref-456' as any + + it('should fetch markup in markup format', async () => { + const mockMarkup = 'Test markup content' + mockCollaborator.getMarkup.mockResolvedValue(mockMarkup) + + const result = await operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'markup') + + const collabId = makeCollabId(objectClass, objectId, objectAttr) + expect(mockCollaborator.getMarkup).toHaveBeenCalledWith(collabId, markupRef) + expect(result).toBe(mockMarkup) + }) + + it('should fetch markup in HTML format', async () => { + const mockMarkup = '

Test content

' + mockCollaborator.getMarkup.mockResolvedValue(mockMarkup) + + const result = await operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'html') + + expect(mockCollaborator.getMarkup).toHaveBeenCalled() + expect(result).toBeDefined() + }) + + it('should fetch markup in markdown format', async () => { + const mockMarkup = '# Test heading' + mockCollaborator.getMarkup.mockResolvedValue(mockMarkup) + + const result = await operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'markdown') + + expect(mockCollaborator.getMarkup).toHaveBeenCalled() + expect(result).toBeDefined() + }) + + it('should throw error for unknown format', async () => { + mockCollaborator.getMarkup.mockResolvedValue('content') + + await expect( + operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'unknown-format' as any) + ).rejects.toThrow('Unknown content format') + }) + + it('should handle collaborator errors', async () => { + mockCollaborator.getMarkup.mockRejectedValue(new Error('Collaborator error')) + + await expect(operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'markup')).rejects.toThrow( + 'Collaborator error' + ) + }) + }) + + describe('uploadMarkup', () => { + const objectClass = 'class:test.Doc' as any + const objectId = 'doc-id-123' as any + const objectAttr = 'content' + const mockMarkupRef = 'new-markup-ref-789' as any + + beforeEach(() => { + mockCollaborator.createMarkup.mockResolvedValue(mockMarkupRef) + }) + + it('should upload markup in markup format', async () => { + const content = 'Test markup content' + + const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, content, 'markup') + + const collabId = makeCollabId(objectClass, objectId, objectAttr) + expect(mockCollaborator.createMarkup).toHaveBeenCalledWith(collabId, content) + expect(result).toBe(mockMarkupRef) + }) + + it('should upload markup in HTML format', async () => { + const content = '

Test HTML content

' + + const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, content, 'html') + + expect(mockCollaborator.createMarkup).toHaveBeenCalled() + expect(result).toBe(mockMarkupRef) + }) + + it('should upload markup in markdown format', async () => { + const content = '# Test markdown' + + const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, content, 'markdown') + + expect(mockCollaborator.createMarkup).toHaveBeenCalled() + expect(result).toBe(mockMarkupRef) + }) + + it('should throw error for unknown format', async () => { + await expect( + operations.uploadMarkup(objectClass, objectId, objectAttr, 'content', 'unknown-format' as any) + ).rejects.toThrow('Unknown content format') + }) + + it('should handle empty content', async () => { + const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, '', 'markup') + + const collabId = makeCollabId(objectClass, objectId, objectAttr) + expect(mockCollaborator.createMarkup).toHaveBeenCalledWith(collabId, '') + expect(result).toBe(mockMarkupRef) + }) + + it('should handle collaborator errors', async () => { + mockCollaborator.createMarkup.mockRejectedValue(new Error('Upload failed')) + + await expect(operations.uploadMarkup(objectClass, objectId, objectAttr, 'content', 'markup')).rejects.toThrow( + 'Upload failed' + ) + }) + }) + + describe('initialization', () => { + it('should initialize collaborator client with correct parameters', () => { + expect(getClient).toHaveBeenCalledWith(workspace, token, mockConfig.COLLABORATOR_URL) + }) + + it('should handle different workspace IDs', () => { + const differentWorkspace = 'different-workspace' as any + createMarkupOperations(url, differentWorkspace, token, mockConfig) + + expect(getClient).toHaveBeenCalledWith(differentWorkspace, token, mockConfig.COLLABORATOR_URL) + }) + + it('should handle different tokens', () => { + const differentToken = 'different-token' + createMarkupOperations(url, workspace, differentToken, mockConfig) + + expect(getClient).toHaveBeenCalledWith(workspace, differentToken, mockConfig.COLLABORATOR_URL) + }) + }) +}) diff --git a/foundations/core/packages/api-client/src/__tests__/markup-types.test.ts b/foundations/core/packages/api-client/src/__tests__/markup-types.test.ts new file mode 100644 index 0000000000..780b0c2ef5 --- /dev/null +++ b/foundations/core/packages/api-client/src/__tests__/markup-types.test.ts @@ -0,0 +1,124 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// + +import { html, markdown, MarkupContent } from '../markup/types' + +describe('MarkupContent', () => { + describe('constructor', () => { + it('should create MarkupContent with content and kind', () => { + const content = '

Hello World

' + const markup = new MarkupContent(content, 'html') + + expect(markup.content).toBe(content) + expect(markup.kind).toBe('html') + }) + + it('should create MarkupContent with markdown kind', () => { + const content = '# Hello World' + const markup = new MarkupContent(content, 'markdown') + + expect(markup.content).toBe(content) + expect(markup.kind).toBe('markdown') + }) + + it('should create MarkupContent with markup kind', () => { + const content = 'plain markup content' + const markup = new MarkupContent(content, 'markup') + + expect(markup.content).toBe(content) + expect(markup.kind).toBe('markup') + }) + }) + + describe('html helper', () => { + it('should create HTML MarkupContent', () => { + const content = '

Title

Content

' + const markup = html(content) + + expect(markup).toBeInstanceOf(MarkupContent) + expect(markup.content).toBe(content) + expect(markup.kind).toBe('html') + }) + + it('should handle empty HTML', () => { + const markup = html('') + + expect(markup.content).toBe('') + expect(markup.kind).toBe('html') + }) + + it('should handle complex HTML with attributes', () => { + const content = '' + const markup = html(content) + + expect(markup.content).toBe(content) + expect(markup.kind).toBe('html') + }) + }) + + describe('markdown helper', () => { + it('should create Markdown MarkupContent', () => { + const content = '# Heading\n\n* List item 1\n* List item 2' + const markup = markdown(content) + + expect(markup).toBeInstanceOf(MarkupContent) + expect(markup.content).toBe(content) + expect(markup.kind).toBe('markdown') + }) + + it('should handle empty markdown', () => { + const markup = markdown('') + + expect(markup.content).toBe('') + expect(markup.kind).toBe('markdown') + }) + + it('should handle markdown with code blocks', () => { + const content = '```javascript\nconst x = 42;\n```' + const markup = markdown(content) + + expect(markup.content).toBe(content) + expect(markup.kind).toBe('markdown') + }) + + it('should handle markdown with links', () => { + const content = '[Link text](https://example.com)' + const markup = markdown(content) + + expect(markup.content).toBe(content) + expect(markup.kind).toBe('markdown') + }) + }) + + describe('edge cases', () => { + it('should handle special characters in content', () => { + const content = '

Special chars: & < > " \'

' + const markup = html(content) + + expect(markup.content).toBe(content) + }) + + it('should handle Unicode characters', () => { + const content = '# 你好世界 🌍' + const markup = markdown(content) + + expect(markup.content).toBe(content) + }) + + it('should handle very long content', () => { + const content = 'a'.repeat(10000) + const markup = html(content) + + expect(markup.content).toBe(content) + expect(markup.content.length).toBe(10000) + }) + + it('should handle multiline content', () => { + const content = 'Line 1\nLine 2\nLine 3' + const markup = markdown(content) + + expect(markup.content).toBe(content) + }) + }) +}) diff --git a/foundations/core/packages/api-client/src/__tests__/rest-utils.test.ts b/foundations/core/packages/api-client/src/__tests__/rest-utils.test.ts new file mode 100644 index 0000000000..620872493e --- /dev/null +++ b/foundations/core/packages/api-client/src/__tests__/rest-utils.test.ts @@ -0,0 +1,209 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// + +import { withRetry, extractJson } from '../rest/utils' + +describe('withRetry', () => { + it('should return result on first success', async () => { + const fn = jest.fn().mockResolvedValue('success') + + const result = await withRetry(fn) + + expect(result).toBe('success') + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('should retry on failure and eventually succeed', async () => { + const fn = jest + .fn() + .mockRejectedValueOnce(new Error('fail 1')) + .mockRejectedValueOnce(new Error('fail 2')) + .mockResolvedValue('success') + + const result = await withRetry(fn) + + expect(result).toBe('success') + expect(fn).toHaveBeenCalledTimes(3) + }) + + it('should throw error after max retries', async () => { + const error = new Error('persistent failure') + const fn = jest.fn().mockRejectedValue(error) + + await expect(withRetry(fn)).rejects.toThrow('persistent failure') + expect(fn).toHaveBeenCalledTimes(3) + }) + + it('should use exponential backoff', async () => { + const delays: number[] = [] + const startTimes: number[] = [] + let lastTime = Date.now() + + const fn = jest.fn(async () => { + const now = Date.now() + if (startTimes.length > 0) { + delays.push(now - lastTime) + } + startTimes.push(now) + lastTime = now + throw new Error('fail') + }) + + await expect(withRetry(fn)).rejects.toThrow('fail') + + expect(fn).toHaveBeenCalledTimes(3) + // Delays should be approximately 100ms and 200ms (with some tolerance) + expect(delays.length).toBe(2) + expect(delays[0]).toBeGreaterThanOrEqual(80) + expect(delays[0]).toBeLessThan(150) + expect(delays[1]).toBeGreaterThanOrEqual(180) + expect(delays[1]).toBeLessThan(250) + }) + + it('should not decrement attempt when ignoreAttemptCheck returns true', async () => { + let callCount = 0 + const fn = jest.fn(async () => { + callCount++ + if (callCount <= 5) { + throw new Error('ignore') + } + throw new Error('real error') + }) + + const ignoreCheck = jest.fn((err: any) => err.message === 'ignore') + + await expect(withRetry(fn, ignoreCheck)).rejects.toThrow('real error') + + // Should have tried more than 3 times because ignored errors don't count + expect(fn.mock.calls.length).toBeGreaterThan(3) + expect(ignoreCheck).toHaveBeenCalled() + }) + + it('should handle promise rejection', async () => { + const fn = jest.fn(async () => { + throw new Error('async error') + }) + + await expect(withRetry(fn)).rejects.toThrow('async error') + expect(fn).toHaveBeenCalledTimes(3) + }) + + it('should handle errors that bypass the check', async () => { + const fn = jest.fn(async () => { + // Always throw error + throw new Error('persistent error') + }) + + const ignoreCheck = jest.fn(() => false) // Never ignore + + await expect(withRetry(fn, ignoreCheck)).rejects.toThrow('persistent error') + expect(fn).toHaveBeenCalledTimes(3) + expect(ignoreCheck).toHaveBeenCalledTimes(3) + }) +}) + +describe('extractJson', () => { + it('should extract plain JSON', async () => { + const mockResponse = { + headers: { + get: jest.fn().mockReturnValue(null) + }, + text: jest.fn().mockResolvedValue('{"key":"value"}') + } as any + + const result = await extractJson(mockResponse) + + expect(result).toEqual({ key: 'value' }) + expect(mockResponse.headers.get).toHaveBeenCalledWith('content-encoding') + }) + + it('should handle TotalArray dataType', async () => { + const jsonString = JSON.stringify({ + dataType: 'TotalArray', + value: [{ id: 1 }, { id: 2 }], + total: 10, + lookupMap: { key: 'value' } + }) + + const mockResponse = { + headers: { + get: jest.fn().mockReturnValue(null) + }, + text: jest.fn().mockResolvedValue(jsonString) + } as any + + const result = await extractJson(mockResponse) + + expect(Array.isArray(result)).toBe(true) + expect(result.total).toBe(10) + expect(result.lookupMap).toEqual({ key: 'value' }) + expect(result[0]).toEqual({ id: 1 }) + }) + + it('should handle snappy encoding header', async () => { + // For this test, we'll just verify the snappy path is attempted + // Actual snappy compression/decompression would require valid compressed data + const mockResponse = { + headers: { + get: jest.fn().mockReturnValue('snappy') + }, + arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(0)) + } as any + + // This will fail to decompress, but we're testing that the snappy path is taken + await expect(extractJson(mockResponse)).rejects.toThrow() + + expect(mockResponse.headers.get).toHaveBeenCalledWith('content-encoding') + expect(mockResponse.arrayBuffer).toHaveBeenCalled() + }) + + it('should handle empty JSON object', async () => { + const mockResponse = { + headers: { + get: jest.fn().mockReturnValue(null) + }, + text: jest.fn().mockResolvedValue('{}') + } as any + + const result = await extractJson(mockResponse) + + expect(result).toEqual({}) + }) + + it('should handle nested objects', async () => { + const nestedData = { + level1: { + level2: { + dataType: 'TotalArray', + value: [1, 2, 3], + total: 3, + lookupMap: {} + } + } + } + + const mockResponse = { + headers: { + get: jest.fn().mockReturnValue(null) + }, + text: jest.fn().mockResolvedValue(JSON.stringify(nestedData)) + } as any + + const result = await extractJson(mockResponse) + + expect(result.level1.level2.total).toBe(3) + expect(Array.isArray(result.level1.level2)).toBe(true) + }) + + it('should throw error on invalid JSON', async () => { + const mockResponse = { + headers: { + get: jest.fn().mockReturnValue(null) + }, + text: jest.fn().mockResolvedValue('invalid json{') + } as any + + await expect(extractJson(mockResponse)).rejects.toThrow() + }) +}) diff --git a/foundations/core/packages/api-client/src/__tests__/utils.test.ts b/foundations/core/packages/api-client/src/__tests__/utils.test.ts new file mode 100644 index 0000000000..68d6d676ba --- /dev/null +++ b/foundations/core/packages/api-client/src/__tests__/utils.test.ts @@ -0,0 +1,202 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// + +import { getWorkspaceToken } from '../utils' +import { loadServerConfig } from '../config' +import { getClient as getAccountClient } from '@hcengineering/account-client' + +// Mock dependencies +jest.mock('../config') +jest.mock('@hcengineering/account-client') + +describe('getWorkspaceToken', () => { + const mockConfig = { + ACCOUNTS_URL: 'https://accounts.example.com', + COLLABORATOR_URL: 'https://collaborator.example.com', + FILES_URL: 'https://files.example.com', + UPLOAD_URL: 'https://upload.example.com' + } + + const mockWorkspaceInfo = { + endpoint: 'wss://workspace.example.com', + token: 'workspace-token-123', + workspace: 'workspace-id-123' as any, + email: 'user@example.com', + workspaceId: 'workspace-id-123', + workspaceName: 'Test Workspace', + workspaceUrl: 'https://workspace.example.com', + createdOn: Date.now(), + lastVisit: Date.now(), + role: 0, + account: 'account-id-123' as any + } + + const mockLoginInfo = { + token: 'login-token-456', + endpoint: 'wss://endpoint.example.com' + } + + let mockAccountClient: any + + beforeEach(() => { + jest.clearAllMocks() + + mockAccountClient = { + login: jest.fn().mockResolvedValue(mockLoginInfo), + selectWorkspace: jest.fn().mockResolvedValue(mockWorkspaceInfo) + } + ;(getAccountClient as jest.Mock).mockReturnValue(mockAccountClient) + ;(loadServerConfig as jest.Mock).mockResolvedValue(mockConfig) + }) + + describe('with email/password authentication', () => { + it('should successfully get workspace token with credentials', async () => { + const result = await getWorkspaceToken('https://api.example.com', { + email: 'user@example.com', + password: 'password123', + workspace: 'test-workspace' + }) + + expect(loadServerConfig).toHaveBeenCalledWith('https://api.example.com') + expect(mockAccountClient.login).toHaveBeenCalledWith('user@example.com', 'password123') + expect(mockAccountClient.selectWorkspace).toHaveBeenCalledWith('test-workspace') + + expect(result).toEqual({ + endpoint: mockWorkspaceInfo.endpoint, + token: mockWorkspaceInfo.token, + workspaceId: mockWorkspaceInfo.workspace, + info: mockWorkspaceInfo + }) + }) + + it('should use provided config if available', async () => { + await getWorkspaceToken( + 'https://api.example.com', + { + email: 'user@example.com', + password: 'password123', + workspace: 'test-workspace' + }, + mockConfig + ) + + expect(loadServerConfig).not.toHaveBeenCalled() + expect(getAccountClient).toHaveBeenCalledWith(mockConfig.ACCOUNTS_URL) + }) + + it('should throw error when login fails', async () => { + mockAccountClient.login.mockResolvedValue({ token: undefined }) + + await expect( + getWorkspaceToken('https://api.example.com', { + email: 'user@example.com', + password: 'wrong-password', + workspace: 'test-workspace' + }) + ).rejects.toThrow('Login failed') + }) + + it('should throw error when workspace not found', async () => { + mockAccountClient.selectWorkspace.mockResolvedValue(undefined) + + await expect( + getWorkspaceToken('https://api.example.com', { + email: 'user@example.com', + password: 'password123', + workspace: 'non-existent-workspace' + }) + ).rejects.toThrow('Workspace not found') + }) + }) + + describe('with token authentication', () => { + it('should successfully get workspace token with existing token', async () => { + const result = await getWorkspaceToken('https://api.example.com', { + token: 'existing-token-789', + workspace: 'test-workspace' + }) + + expect(mockAccountClient.login).not.toHaveBeenCalled() + expect(getAccountClient).toHaveBeenCalledWith(mockConfig.ACCOUNTS_URL, 'existing-token-789') + expect(mockAccountClient.selectWorkspace).toHaveBeenCalledWith('test-workspace') + + expect(result).toEqual({ + endpoint: mockWorkspaceInfo.endpoint, + token: mockWorkspaceInfo.token, + workspaceId: mockWorkspaceInfo.workspace, + info: mockWorkspaceInfo + }) + }) + + it('should throw error when workspace not found with token', async () => { + mockAccountClient.selectWorkspace.mockResolvedValue(undefined) + + await expect( + getWorkspaceToken('https://api.example.com', { + token: 'existing-token-789', + workspace: 'non-existent-workspace' + }) + ).rejects.toThrow('Workspace not found') + }) + }) + + describe('error handling', () => { + it('should propagate config loading errors', async () => { + ;(loadServerConfig as jest.Mock).mockRejectedValue(new Error('Config load failed')) + + await expect( + getWorkspaceToken('https://api.example.com', { + email: 'user@example.com', + password: 'password123', + workspace: 'test-workspace' + }) + ).rejects.toThrow('Config load failed') + }) + + it('should propagate login errors', async () => { + mockAccountClient.login.mockRejectedValue(new Error('Invalid credentials')) + + await expect( + getWorkspaceToken('https://api.example.com', { + email: 'user@example.com', + password: 'wrong-password', + workspace: 'test-workspace' + }) + ).rejects.toThrow('Invalid credentials') + }) + + it('should propagate workspace selection errors', async () => { + mockAccountClient.selectWorkspace.mockRejectedValue(new Error('Access denied')) + + await expect( + getWorkspaceToken('https://api.example.com', { + email: 'user@example.com', + password: 'password123', + workspace: 'test-workspace' + }) + ).rejects.toThrow('Access denied') + }) + }) + + describe('edge cases', () => { + it('should handle empty workspace name', async () => { + await getWorkspaceToken('https://api.example.com', { + token: 'token', + workspace: '' + }) + + expect(mockAccountClient.selectWorkspace).toHaveBeenCalledWith('') + }) + + it('should handle special characters in credentials', async () => { + await getWorkspaceToken('https://api.example.com', { + email: 'user+test@example.com', + password: 'p@ssw0rd!#$%', + workspace: 'test-workspace' + }) + + expect(mockAccountClient.login).toHaveBeenCalledWith('user+test@example.com', 'p@ssw0rd!#$%') + }) + }) +}) diff --git a/foundations/core/packages/api-client/src/client.ts b/foundations/core/packages/api-client/src/client.ts new file mode 100644 index 0000000000..118fde1760 --- /dev/null +++ b/foundations/core/packages/api-client/src/client.ts @@ -0,0 +1,297 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { getClient as getAccountClient } from '@hcengineering/account-client' +import client, { clientId } from '@hcengineering/client' +import { + type Account, + type Class, + type Client, + type Data, + type Doc, + type DocumentQuery, + type FindOptions, + type FindResult, + type Hierarchy, + type ModelDb, + type Ref, + type Space, + type TxResult, + type WithLookup, + AttachedData, + AttachedDoc, + DocumentUpdate, + Mixin, + MixinData, + MixinUpdate, + TxOperations, + WorkspaceUuid, + generateId, + pickPrimarySocialId +} from '@hcengineering/core' +import { addLocation, getResource } from '@hcengineering/platform' + +import { type ServerConfig, loadServerConfig } from './config' +import { + type MarkupFormat, + type MarkupOperations, + type MarkupRef, + MarkupContent, + createMarkupOperations +} from './markup' +import { type ConnectOptions, type PlatformClient, WithMarkup } from './types' +import { getWorkspaceToken } from './utils' + +/** + * Create platform client + * @public */ +export async function connect (url: string, options: ConnectOptions): Promise { + const config = await loadServerConfig(url) + + const { endpoint, token } = await getWorkspaceToken(url, options, config) + const accountClient = getAccountClient(config.ACCOUNTS_URL, token) + const socialIds = await accountClient.getSocialIds(true) + const wsLoginInfo = await accountClient.selectWorkspace(options.workspace) + + if (wsLoginInfo === undefined) { + throw new Error(`Workspace ${options.workspace} not found`) + } + + const account: Account = { + uuid: wsLoginInfo.account, + role: wsLoginInfo.role, + primarySocialId: pickPrimarySocialId(socialIds)._id, + socialIds: socialIds.map((si) => si._id), + fullSocialIds: socialIds + } + + return await createClient(url, endpoint, token, wsLoginInfo.workspace, account, config, options) +} + +async function createClient ( + url: string, + endpoint: string, + token: string, + workspaceUuid: WorkspaceUuid, + account: Account, + config: ServerConfig, + options: ConnectOptions +): Promise { + addLocation(clientId, () => import(/* webpackChunkName: "client" */ '@hcengineering/client-resources')) + + const { socketFactory, connectionTimeout } = options + + const clientFactory = await getResource(client.function.GetClient) + const connection = await clientFactory(token, endpoint, { + socketFactory, + connectionTimeout + }) + + return new PlatformClientImpl(url, workspaceUuid, token, config, connection, account) +} + +class PlatformClientImpl implements PlatformClient { + private readonly client: TxOperations + private readonly markup: MarkupOperations + + constructor ( + private readonly url: string, + private readonly workspace: WorkspaceUuid, + private readonly token: string, + private readonly config: ServerConfig, + private readonly connection: Client, + private readonly account: Account + ) { + this.client = new TxOperations(connection, account.primarySocialId) + this.markup = createMarkupOperations(url, workspace, token, config) + } + + // Client + + getHierarchy (): Hierarchy { + return this.client.getHierarchy() + } + + getModel (): ModelDb { + return this.client.getModel() + } + + async getAccount (): Promise { + return this.account + } + + async findOne( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise | undefined> { + return await this.client.findOne(_class, query, options) + } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + return await this.client.findAll(_class, query, options) + } + + async close (): Promise { + await this.connection.close() + } + + private async processMarkup(_class: Ref>, id: Ref, data: WithMarkup): Promise { + const result: any = {} + + for (const [key, value] of Object.entries(data)) { + if (value instanceof MarkupContent) { + result[key] = this.markup.uploadMarkup(_class, id, key, value.content, value.kind) + } else { + result[key] = value + } + } + + return result as T + } + + // DocOperations + + async createDoc( + _class: Ref>, + space: Ref, + attributes: WithMarkup>, + id?: Ref + ): Promise> { + id ??= generateId() + const data = await this.processMarkup>(_class, id, attributes) + return await this.client.createDoc(_class, space, data, id) + } + + async updateDoc( + _class: Ref>, + space: Ref, + objectId: Ref, + operations: WithMarkup>, + retrieve?: boolean + ): Promise { + const update = await this.processMarkup>(_class, objectId, operations) + return await this.client.updateDoc(_class, space, objectId, update, retrieve) + } + + async removeDoc(_class: Ref>, space: Ref, objectId: Ref): Promise { + return await this.client.removeDoc(_class, space, objectId) + } + + // CollectionOperations + + async addCollection( + _class: Ref>, + space: Ref, + attachedTo: Ref, + attachedToClass: Ref>, + collection: Extract | string, + attributes: WithMarkup>, + id?: Ref

+ ): Promise> { + id ??= generateId() + const data = await this.processMarkup>(_class, id, attributes) + return await this.client.addCollection(_class, space, attachedTo, attachedToClass, collection, data, id) + } + + async updateCollection( + _class: Ref>, + space: Ref, + objectId: Ref

, + attachedTo: Ref, + attachedToClass: Ref>, + collection: Extract | string, + operations: WithMarkup>, + retrieve?: boolean + ): Promise> { + const update = await this.processMarkup>(_class, objectId, operations) + return await this.client.updateCollection( + _class, + space, + objectId, + attachedTo, + attachedToClass, + collection, + update, + retrieve + ) + } + + async removeCollection( + _class: Ref>, + space: Ref, + objectId: Ref

, + attachedTo: Ref, + attachedToClass: Ref>, + collection: Extract | string + ): Promise> { + return await this.client.removeCollection(_class, space, objectId, attachedTo, attachedToClass, collection) + } + + // MixinOperations + + async createMixin( + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + mixin: Ref>, + attributes: WithMarkup> + ): Promise { + const data = await this.processMarkup>(objectClass, objectId, attributes) + return await this.client.createMixin(objectId, objectClass, objectSpace, mixin, data) + } + + async updateMixin( + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + mixin: Ref>, + attributes: WithMarkup> + ): Promise { + const update = await this.processMarkup>(objectClass, objectId, attributes) + return await this.client.updateMixin(objectId, objectClass, objectSpace, mixin, update) + } + + // Markup + + async fetchMarkup ( + objectClass: Ref>, + objectId: Ref, + objectAttr: string, + markup: MarkupRef, + format: MarkupFormat + ): Promise { + return await this.markup.fetchMarkup(objectClass, objectId, objectAttr, markup, format) + } + + async uploadMarkup ( + objectClass: Ref>, + objectId: Ref, + objectAttr: string, + markup: string, + format: MarkupFormat + ): Promise { + return await this.markup.uploadMarkup(objectClass, objectId, objectAttr, markup, format) + } + + // AsyncDisposable + + async [Symbol.asyncDispose] (): Promise { + await this.close() + } +} diff --git a/foundations/core/packages/api-client/src/config.ts b/foundations/core/packages/api-client/src/config.ts new file mode 100644 index 0000000000..9386748a38 --- /dev/null +++ b/foundations/core/packages/api-client/src/config.ts @@ -0,0 +1,32 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { concatLink } from '@hcengineering/core' + +export interface ServerConfig { + ACCOUNTS_URL: string + COLLABORATOR_URL: string + FILES_URL: string + UPLOAD_URL: string +} + +export async function loadServerConfig (url: string): Promise { + const configUrl = concatLink(url, '/config.json') + const res = await fetch(configUrl, { keepalive: true }) + if (res.ok) { + return (await res.json()) as ServerConfig + } + throw new Error('Failed to fetch config') +} diff --git a/foundations/core/packages/api-client/src/index.ts b/foundations/core/packages/api-client/src/index.ts new file mode 100644 index 0000000000..2379b838de --- /dev/null +++ b/foundations/core/packages/api-client/src/index.ts @@ -0,0 +1,23 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './client' +export * from './markup/types' +export * from './socket' +export * from './types' +export * from './rest' +export * from './config' +export * from './utils' +export * from './storage' diff --git a/foundations/core/packages/api-client/src/markup/client.ts b/foundations/core/packages/api-client/src/markup/client.ts new file mode 100644 index 0000000000..5a783a2080 --- /dev/null +++ b/foundations/core/packages/api-client/src/markup/client.ts @@ -0,0 +1,106 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type Class, + type Doc, + type Markup, + type Ref, + WorkspaceUuid, + concatLink, + makeCollabId +} from '@hcengineering/core' +import { type CollaboratorClient, getClient } from '@hcengineering/collaborator-client' +import { htmlToJSON, jsonToHTML, jsonToMarkup, markupToJSON } from '@hcengineering/text' +import { markdownToMarkup, markupToMarkdown } from '@hcengineering/text-markdown' + +import { type ServerConfig } from '../config' +import { type MarkupOperations, type MarkupFormat, type MarkupRef } from './types' + +export function createMarkupOperations ( + url: string, + workspace: WorkspaceUuid, + token: string, + config: ServerConfig +): MarkupOperations { + return new MarkupOperationsImpl(url, workspace, token, config) +} + +class MarkupOperationsImpl implements MarkupOperations { + private readonly collaborator: CollaboratorClient + private readonly imageUrl: string + private readonly refUrl: string + + constructor ( + private readonly url: string, + private readonly workspace: WorkspaceUuid, + private readonly token: string, + private readonly config: ServerConfig + ) { + this.refUrl = concatLink(this.url, `/browse?workspace=${workspace}`) + this.imageUrl = concatLink(this.url, `/files?workspace=${workspace}&file=`) + this.collaborator = getClient(workspace, token, config.COLLABORATOR_URL) + } + + async fetchMarkup ( + objectClass: Ref>, + objectId: Ref, + objectAttr: string, + doc: MarkupRef, + format: MarkupFormat + ): Promise { + const collabId = makeCollabId(objectClass, objectId, objectAttr) + const markup = await this.collaborator.getMarkup(collabId, doc) + const json = markupToJSON(markup) + + switch (format) { + case 'markup': + return markup + case 'html': + return jsonToHTML(json) + case 'markdown': + return markupToMarkdown(json, { refUrl: this.refUrl, imageUrl: this.imageUrl }) + default: + throw new Error('Unknown content format') + } + } + + async uploadMarkup ( + objectClass: Ref>, + objectId: Ref, + objectAttr: string, + value: string, + format: MarkupFormat + ): Promise { + let markup: Markup = '' + + switch (format) { + case 'markup': + markup = value + break + case 'html': + markup = jsonToMarkup(htmlToJSON(value)) + break + case 'markdown': + markup = jsonToMarkup(markdownToMarkup(value, { refUrl: this.refUrl, imageUrl: this.imageUrl })) + break + default: + throw new Error('Unknown content format') + } + + const collabId = makeCollabId(objectClass, objectId, objectAttr) + return await this.collaborator.createMarkup(collabId, markup) + } +} diff --git a/foundations/core/packages/api-client/src/markup/index.ts b/foundations/core/packages/api-client/src/markup/index.ts new file mode 100644 index 0000000000..3c18f70583 --- /dev/null +++ b/foundations/core/packages/api-client/src/markup/index.ts @@ -0,0 +1,17 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './client' +export * from './types' diff --git a/foundations/core/packages/api-client/src/markup/types.ts b/foundations/core/packages/api-client/src/markup/types.ts new file mode 100644 index 0000000000..a4f6765689 --- /dev/null +++ b/foundations/core/packages/api-client/src/markup/types.ts @@ -0,0 +1,79 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Class, type Blob, type Doc, type Ref } from '@hcengineering/core' + +/** @public */ +export type MarkupRef = Ref + +/** @public */ +export type MarkupFormat = 'markup' | 'html' | 'markdown' + +/** @public */ +export class MarkupContent { + constructor ( + readonly content: string, + readonly kind: MarkupFormat + ) {} +} + +/** @public */ +export function html (content: string): MarkupContent { + return new MarkupContent(content, 'html') +} + +/** @public */ +export function markdown (content: string): MarkupContent { + return new MarkupContent(content, 'markdown') +} + +/** + * Provides operations for managing markup (rich-text) content. + * @public */ +export interface MarkupOperations { + /** + * Retrieves markup content for a specified document object + * * @param objectClass - Reference to the class of the document containing the markup + * @param objectId - Reference to the document containing the markup + * @param objectAttr - The attribute/field name where the markup is stored + * @param id - Unique reference identifying the specific markup content + * @param format - The format of the markup (e.g., HTML, Markdown, etc.) + * @returns Promise containing the markup content as a string + */ + fetchMarkup: ( + objectClass: Ref>, + objectId: Ref, + objectAttr: string, + id: MarkupRef, + format: MarkupFormat + ) => Promise + + /** + * Saves markup content for a document object + * @param objectClass - Reference to the class of the document where markup should be stored + * @param objectId - Reference to the document where markup should be stored + * @param objectAttr - The attribute/field name where markup should be saved + * @param markup - The actual markup content to be uploaded + * @param format - The format of the provided markup (e.g., HTML, Markdown, etc.) + * @returns Promise containing a reference to the newly saved markup + */ + uploadMarkup: ( + objectClass: Ref>, + objectId: Ref, + objectAttr: string, + markup: string, + format: MarkupFormat + ) => Promise +} diff --git a/foundations/core/packages/api-client/src/rest/index.ts b/foundations/core/packages/api-client/src/rest/index.ts new file mode 100644 index 0000000000..371187c27a --- /dev/null +++ b/foundations/core/packages/api-client/src/rest/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export { createRestClient, connectRest } from './rest' +export { createRestTxOperations } from './tx' +export * from './types' diff --git a/foundations/core/packages/api-client/src/rest/rest.ts b/foundations/core/packages/api-client/src/rest/rest.ts new file mode 100644 index 0000000000..827e2dbbf3 --- /dev/null +++ b/foundations/core/packages/api-client/src/rest/rest.ts @@ -0,0 +1,368 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type Account, + buildModel, + type Class, + concatLink, + type Doc, + type DocumentQuery, + type DomainParams, + type DomainRequestOptions, + type DomainResult, + type FindOptions, + type FindResult, + Hierarchy, + MeasureMetricsContext, + ModelDb, + OperationDomain, + PersonId, + PersonUuid, + type Ref, + type SearchOptions, + type SearchQuery, + type SearchResult, + SocialIdType, + type Tx, + type TxResult, + type WithLookup +} from '@hcengineering/core' +import { PlatformError, type Status, unknownError } from '@hcengineering/platform' + +import { AuthOptions } from '../types' +import { getWorkspaceToken } from '../utils' +import type { RestClient } from './types' +import { extractJson, withRetry } from './utils' + +export function createRestClient (endpoint: string, workspaceId: string, token: string): RestClient { + return new RestClientImpl(endpoint, workspaceId, token) +} + +export async function connectRest (url: string, options: AuthOptions): Promise { + const { endpoint, token, workspaceId } = await getWorkspaceToken(url, options) + return createRestClient(endpoint, workspaceId, token) +} + +const rateLimitError = 'rate-limit' + +function isRLE (err: any): boolean { + return err.message === rateLimitError +} + +export class RestClientImpl implements RestClient { + endpoint: string + + slowDownTimer = 0 + currentRateLimit: { remaining: number, limit: number } = { remaining: 1000, limit: 1000 } + + remaining: number = 1000 + limit: number = 1000 + constructor ( + endpoint: string, + readonly workspace: string, + readonly token: string + ) { + this.endpoint = endpoint.replace('ws', 'http') + } + + jsonHeaders (): Record { + return { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.token, + 'accept-encoding': 'snappy, gzip' + } + } + + requestInit (): RequestInit { + return { + method: 'GET', + keepalive: true, + headers: this.jsonHeaders() + } + } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + const params = new URLSearchParams() + params.append('class', _class) + if (query !== undefined && Object.keys(query).length > 0) { + params.append('query', JSON.stringify(query)) + } + if (options !== undefined && Object.keys(options).length > 0) { + params.append('options', JSON.stringify(options)) + } + const requestUrl = concatLink(this.endpoint, `/api/v1/find-all/${this.workspace}?${params.toString()}`) + const result = await withRetry & { error?: Status }>(async () => { + const response = await fetch(requestUrl, this.requestInit()) + if (!response.ok) { + await this.checkRateLimits(response) + throw new PlatformError(unknownError(response.statusText)) + } + this.updateRateLimit(response) + return await extractJson>(response) + }, isRLE) + + if (result.error !== undefined) { + throw new PlatformError(result.error) + } + + if (result.lookupMap !== undefined) { + // We need to extract lookup map to document lookups + for (const d of result) { + if (d.$lookup !== undefined) { + for (const [k, v] of Object.entries(d.$lookup)) { + if (!Array.isArray(v)) { + ;(d as any).$lookup[k] = result.lookupMap[v] + } else { + ;(d as any).$lookup[k] = v.map((it) => result.lookupMap?.[it]) + } + } + } + } + delete result.lookupMap + } + + // We need to revert deleted query simple values. + // We need to get rid of simple query parameters matched in documents + for (const doc of result) { + if (doc._class == null) { + doc._class = _class + } + for (const [k, v] of Object.entries(query)) { + if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') { + if ((doc as any)[k] == null) { + ;(doc as any)[k] = v + } + } + } + } + + return result + } + + private async checkRate (): Promise { + if (this.currentRateLimit.remaining < this.currentRateLimit.limit / 3) { + if (this.slowDownTimer < 50) { + this.slowDownTimer += 50 + } + this.slowDownTimer++ + } else if (this.slowDownTimer > 0) { + this.slowDownTimer-- + } + if (this.slowDownTimer > 0) { + // We need to wait a bit to avoid ban. + await new Promise((resolve) => setTimeout(resolve, this.slowDownTimer)) + } + } + + private updateRateLimit (response: Response): void { + const rateLimitLimit: number = parseInt(response.headers.get('X-RateLimit-Limit') ?? '100') + const remaining: number = parseInt(response.headers.get('X-RateLimit-Remaining') ?? '100') + this.currentRateLimit = { remaining, limit: rateLimitLimit } + } + + private async checkRateLimits (response: Response): Promise { + if (response.status === 429) { + // Extract rate limit information from headers + const retryAfter = response.headers.get('Retry-After') + const retryAfterMS = response.headers.get('Retry-After-ms') + const rateLimitReset = response.headers.get('X-RateLimit-Reset') + + this.updateRateLimit(response) + const waitTime = + (retryAfterMS != null ? parseInt(retryAfterMS) : undefined) ?? + (retryAfter != null + ? parseInt(retryAfter) * 1000 + : rateLimitReset != null + ? new Date(parseInt(rateLimitReset)).getTime() - Date.now() + : 1000) // Default to 1 seconds if no headers are provided + await new Promise((resolve) => setTimeout(resolve, waitTime)) + throw new Error(rateLimitError) + } + } + + async getAccount (): Promise { + const requestUrl = concatLink(this.endpoint, `/api/v1/account/${this.workspace}`) + await this.checkRate() + const result = await withRetry(async () => { + const response = await fetch(requestUrl, this.requestInit()) + if (!response.ok) { + await this.checkRateLimits(response) + throw new PlatformError(unknownError(response.statusText)) + } + this.updateRateLimit(response) + return await extractJson(response) + }) + if (result.error !== undefined) { + throw new PlatformError(result.error) + } + return result + } + + async getModel (full: boolean = false): Promise<{ hierarchy: Hierarchy, model: ModelDb }> { + const requestUrl = new URL(concatLink(this.endpoint, `/api/v1/load-model/${this.workspace}`)) + if (full) { + requestUrl.searchParams.append('full', 'true') + } + await this.checkRate() + const result = await withRetry<{ hierarchy: Hierarchy, model: ModelDb, error?: Status }>(async () => { + const response = await fetch(requestUrl, this.requestInit()) + if (!response.ok) { + await this.checkRateLimits(response) + throw new PlatformError(unknownError(response.statusText)) + } + this.updateRateLimit(response) + + const modelResponse: Tx[] = await extractJson(response) + + const hierarchy = new Hierarchy() + const model = new ModelDb(hierarchy) + + const ctx = new MeasureMetricsContext('loadModel', {}) + buildModel(ctx, modelResponse, undefined, hierarchy, model) + + return { hierarchy, model } + }, isRLE) + if (result.error !== undefined) { + throw new PlatformError(result.error) + } + return result + } + + async findOne( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise | undefined> { + return (await this.findAll(_class, query, { ...options, limit: 1 })).shift() + } + + async tx (tx: Tx): Promise { + const requestUrl = concatLink(this.endpoint, `/api/v1/tx/${this.workspace}`) + await this.checkRate() + const result = await withRetry(async () => { + const response = await fetch(requestUrl, { + method: 'POST', + headers: this.jsonHeaders(), + keepalive: true, + body: JSON.stringify(tx) + }) + if (!response.ok) { + await this.checkRateLimits(response) + throw new PlatformError(unknownError(response.statusText)) + } + this.updateRateLimit(response) + return await extractJson(response) + }, isRLE) + if (result.error !== undefined) { + throw new PlatformError(result.error) + } + return result + } + + async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { + const result = await withRetry(async () => { + const params = new URLSearchParams() + params.append('query', query.query) + if (query.classes != null && Object.keys(query.classes).length > 0) { + params.append('classes', JSON.stringify(query.classes)) + } + if (query.spaces != null && Object.keys(query.spaces).length > 0) { + params.append('spaces', JSON.stringify(query.spaces)) + } + if (options.limit != null) { + params.append('limit', `${options.limit}`) + } + const requestUrl = concatLink(this.endpoint, `/api/v1/search-fulltext/${this.workspace}?${params.toString()}`) + const response = await fetch(requestUrl, { + method: 'GET', + headers: this.jsonHeaders(), + keepalive: true + }) + if (!response.ok) { + await this.checkRateLimits(response) + throw new PlatformError(unknownError(response.statusText)) + } + this.updateRateLimit(response) + return await extractJson(response) + }) + if (result.error !== undefined) { + throw new PlatformError(result.error) + } + return result + } + + async domainRequest( + domain: OperationDomain, + params: DomainParams, + options?: DomainRequestOptions + ): Promise> { + const requestUrl = concatLink(this.endpoint, `/api/v1/request/${domain}/${this.workspace}`) + + await this.checkRate() + return await withRetry(async () => { + const response = await fetch(requestUrl, { + method: 'POST', + headers: this.jsonHeaders(), + keepalive: true, + body: JSON.stringify(params) + }) + if (!response.ok) { + await this.checkRateLimits(response) + throw new PlatformError(unknownError(response.statusText)) + } + this.updateRateLimit(response) + const value = await extractJson(response) + return { domain, value } + }, isRLE) + } + + async ensurePerson ( + socialType: SocialIdType, + socialValue: string, + firstName: string, + lastName: string + ): Promise<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }> { + const requestUrl = concatLink(this.endpoint, `/api/v1/ensure-person/${this.workspace}`) + await this.checkRate() + const result = await withRetry(async () => { + const response = await fetch(requestUrl, { + method: 'POST', + headers: this.jsonHeaders(), + keepalive: true, + body: JSON.stringify({ + socialType, + socialValue, + firstName, + lastName + }) + }) + if (!response.ok) { + await this.checkRateLimits(response) + throw new PlatformError(unknownError(response.statusText)) + } + this.updateRateLimit(response) + return await extractJson<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }>(response) + }, isRLE) + if (result.error !== undefined) { + throw new PlatformError(result.error) + } + return result + } +} diff --git a/foundations/core/packages/api-client/src/rest/tx.ts b/foundations/core/packages/api-client/src/rest/tx.ts new file mode 100644 index 0000000000..d13a68ecea --- /dev/null +++ b/foundations/core/packages/api-client/src/rest/tx.ts @@ -0,0 +1,114 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type Account, + type Class, + type Client, + type Doc, + type DocumentQuery, + type DomainParams, + type DomainRequestOptions, + type DomainResult, + type FindOptions, + type FindResult, + Hierarchy, + ModelDb, + type OperationDomain, + type Ref, + type SearchOptions, + type SearchQuery, + type SearchResult, + toFindResult, + type Tx, + TxOperations, + type TxResult, + type WithLookup +} from '@hcengineering/core' +import { RestClientImpl } from './rest' + +export async function createRestTxOperations ( + endpoint: string, + workspaceId: string, + token: string, + fullModel: boolean = false +): Promise { + const restClient = new RestClientImpl(endpoint, workspaceId, token) + + const account = await restClient.getAccount() + const { hierarchy, model } = await restClient.getModel(fullModel) + + return new TxOperations(new RestTxClient(restClient, hierarchy, model, account), account.socialIds[0]) +} + +class RestTxClient implements Client { + constructor ( + readonly client: RestClientImpl, + readonly hierarchy: Hierarchy, + readonly model: ModelDb, + readonly account: Account + ) {} + + close (): Promise { + return Promise.resolve() + } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + const data = await this.client.findAll(_class, query, options) + const result = data.map((v) => { + return this.hierarchy.updateLookupMixin(_class, v, options) + }) + return toFindResult(result, data.total) + } + + async domainRequest( + domain: OperationDomain, + params: DomainParams, + options?: DomainRequestOptions + ): Promise> { + return await this.client.domainRequest(domain, params, options) + } + + async findOne( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise | undefined> { + const v = await this.client.findOne(_class, query, options) + if (v === undefined) { + return + } + return this.hierarchy.updateLookupMixin(_class, v, options) + } + + getHierarchy: () => Hierarchy = () => this.hierarchy + getModel: () => ModelDb = () => this.model + + async getAccount (): Promise { + return this.account + } + + async tx (tx: Tx): Promise { + return await this.client.tx(tx) + } + + async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { + return await this.client.searchFulltext(query, options) + } +} diff --git a/foundations/core/packages/api-client/src/rest/types.ts b/foundations/core/packages/api-client/src/rest/types.ts new file mode 100644 index 0000000000..d878fcccce --- /dev/null +++ b/foundations/core/packages/api-client/src/rest/types.ts @@ -0,0 +1,60 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type Account, + type Class, + type Doc, + type DocumentQuery, + type DomainParams, + type DomainRequestOptions, + type DomainResult, + type FindOptions, + type FulltextStorage, + type Hierarchy, + type ModelDb, + type OperationDomain, + type PersonId, + type PersonUuid, + type Ref, + type SocialIdType, + type Storage, + type WithLookup +} from '@hcengineering/core' + +export interface RestClient extends Storage, FulltextStorage { + getAccount: () => Promise + + findOne: ( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ) => Promise | undefined> + + getModel: () => Promise<{ hierarchy: Hierarchy, model: ModelDb }> + + domainRequest: ( + domain: OperationDomain, + params: DomainParams, + options?: DomainRequestOptions + ) => Promise> + + ensurePerson: ( + socialType: SocialIdType, + socialValue: string, + firstName: string, + lastName: string + ) => Promise<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }> +} diff --git a/foundations/core/packages/api-client/src/rest/utils.ts b/foundations/core/packages/api-client/src/rest/utils.ts new file mode 100644 index 0000000000..8b098ac7e5 --- /dev/null +++ b/foundations/core/packages/api-client/src/rest/utils.ts @@ -0,0 +1,46 @@ +import { uncompress } from 'snappyjs' + +export async function withRetry (fn: () => Promise, ignoreAttemptCheck?: (err: any) => boolean): Promise { + const maxRetries = 3 + let lastError: any + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await fn() + } catch (err: any) { + if (ignoreAttemptCheck !== undefined && ignoreAttemptCheck(err)) { + // Do not decrement attempt + attempt-- + } else { + lastError = err + } + if (attempt === maxRetries - 1) { + throw lastError + } + await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100)) + } + } + throw lastError +} + +function rpcJSONReceiver (key: string, value: any): any { + if (typeof value === 'object' && value !== null) { + if (value.dataType === 'TotalArray') { + return Object.assign(value.value, { total: value.total, lookupMap: value.lookupMap }) + } + } + return value +} + +export async function extractJson (response: Response): Promise { + const encoding = response.headers.get('content-encoding') + if (encoding === 'snappy') { + const buffer = await response.arrayBuffer() + const decompressed = uncompress(buffer) + const decoder = new TextDecoder() + const jsonString = decoder.decode(decompressed) + return JSON.parse(jsonString, rpcJSONReceiver) as T + } + const jsonString = await response.text() + return JSON.parse(jsonString, rpcJSONReceiver) as T +} diff --git a/foundations/core/packages/api-client/src/socket/browser.ts b/foundations/core/packages/api-client/src/socket/browser.ts new file mode 100644 index 0000000000..a9dcaca512 --- /dev/null +++ b/foundations/core/packages/api-client/src/socket/browser.ts @@ -0,0 +1,22 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type ClientSocket, type ClientSocketFactory } from '@hcengineering/client' + +/** @public */ +export const BrowserWebSocketFactory: ClientSocketFactory = (url: string): ClientSocket => { + const ws = new WebSocket(url) + return ws as ClientSocket +} diff --git a/foundations/core/packages/api-client/src/socket/index.ts b/foundations/core/packages/api-client/src/socket/index.ts new file mode 100644 index 0000000000..bebba34b90 --- /dev/null +++ b/foundations/core/packages/api-client/src/socket/index.ts @@ -0,0 +1,17 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './browser' +export * from './node' diff --git a/foundations/core/packages/api-client/src/socket/node.ts b/foundations/core/packages/api-client/src/socket/node.ts new file mode 100644 index 0000000000..5aac1f53d7 --- /dev/null +++ b/foundations/core/packages/api-client/src/socket/node.ts @@ -0,0 +1,102 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type ClientSocket, type ClientSocketFactory } from '@hcengineering/client' + +/** @public */ +export const NodeWebSocketFactory: ClientSocketFactory = (url: string): ClientSocket => { + // We need to override default factory with 'ws' one. + // eslint-disable-next-line + let WebSocket + try { + WebSocket = require('ws') + } catch (error) { + throw new Error('The "ws" package is required for NodeWebSocketFactory. ') + } + type WebSocketData = Parameters[1] + + const ws = new WebSocket(url) + + const client: ClientSocket = { + get readyState (): number { + return ws.readyState + }, + + send: (data: string | ArrayBufferLike | Blob | ArrayBufferView): void => { + if (data instanceof Blob) { + void data.arrayBuffer().then((buffer) => { + ws.send(buffer) + }) + } else { + ws.send(data) + } + }, + + close: (code?: number): void => { + ws.close(code) + } + } + + ws.on('message', (data: WebSocketData) => { + if (client.onmessage != null) { + const event = { + data, + type: 'message', + target: this + } as unknown as MessageEvent + + client.onmessage(event) + } + }) + + ws.on('close', (code: number, reason: string) => { + if (client.onclose != null) { + const closeEvent = { + code, + reason, + wasClean: code === 1000, + type: 'close', + target: this + } as unknown as CloseEvent + + client.onclose(closeEvent) + } + }) + + ws.on('open', () => { + if (client.onopen != null) { + const event = { + type: 'open', + target: this + } as unknown as Event + + client.onopen(event) + } + }) + + ws.on('error', (error: Error) => { + if (client.onerror != null) { + const event = { + type: 'error', + target: this, + error + } as unknown as Event + + client.onerror(event) + } + }) + + return client +} diff --git a/foundations/core/packages/api-client/src/storage/client.ts b/foundations/core/packages/api-client/src/storage/client.ts new file mode 100644 index 0000000000..025d7d8ac5 --- /dev/null +++ b/foundations/core/packages/api-client/src/storage/client.ts @@ -0,0 +1,220 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { concatLink, WorkspaceUuid, Blob, Ref } from '@hcengineering/core' +import { Readable } from 'stream' +import { StorageClient } from './types' +import { loadServerConfig, ServerConfig } from '../config' +import { NetworkError, NotFoundError, StorageError } from './error' +import { AuthOptions } from '../types' +import { getWorkspaceToken } from '../utils' + +interface ObjectMetadata { + name: string + etag: string + size: number + contentType: string + lastModified: number + cacheControl?: string +} + +interface BlobUploadSuccess { + key: string + id: string + metadata: ObjectMetadata +} + +interface BlobUploadError { + key: string + error: string +} + +type BlobUploadResult = BlobUploadSuccess | BlobUploadError + +export class StorageClientImpl implements StorageClient { + private readonly headers: Record + constructor ( + readonly filesUrl: string, + readonly uploadUrl: string, + token: string, + readonly workspace: WorkspaceUuid + ) { + this.headers = { + Authorization: 'Bearer ' + token + } + } + + getObjectUrl (objectName: string): string { + return this.filesUrl.replace(':filename', objectName).replace(':blobId', objectName) + } + + async stat (objectName: string): Promise { + const url = this.getObjectUrl(objectName) + let response + try { + response = await wrappedFetch(url, { method: 'HEAD', headers: { ...this.headers } }) + } catch (error: any) { + if (error instanceof NotFoundError) { + return + } + throw error + } + const headers = response.headers + const lastModified = Date.parse(headers.get('Last-Modified') ?? '') + const size = parseInt(headers.get('Content-Length') ?? '0', 10) + return { + provider: '', + _class: core.class.Blob, + _id: objectName as Ref, + contentType: headers.get('Content-Type') ?? '', + size: isNaN(size) ? 0 : (size ?? 0), + etag: headers.get('ETag') ?? '', + space: core.space.Configuration, + modifiedBy: core.account.System, + modifiedOn: isNaN(lastModified) ? 0 : lastModified, + version: null + } + } + + async get (objectName: string): Promise { + const url = this.getObjectUrl(objectName) + + const response = await wrappedFetch(url, { headers: { ...this.headers } }) + + if (response.body == null) { + throw new StorageError('Missing response body') + } + return Readable.from(response.body) + } + + async put (objectName: string, stream: Readable | Buffer | string, contentType: string, size?: number): Promise { + const buffer = await toBuffer(stream) + const file = new File([new Uint8Array(buffer)], objectName, { type: contentType }) + const formData = new FormData() + formData.append('file', file) + let response + try { + response = await fetch(this.uploadUrl, { + method: 'POST', + body: formData, + headers: { ...this.headers } + }) + } catch (error: any) { + throw new NetworkError(`Network error ${error}`) + } + if (!response.ok) { + throw new StorageError(await response.text()) + } + const result = (await response.json()) as BlobUploadResult[] + if (Object.hasOwn(result[0], 'id')) { + const fileResult = result[0] as BlobUploadSuccess + return { + _class: core.class.Blob, + _id: fileResult.id as Ref, + space: core.space.Configuration, + modifiedOn: fileResult.metadata.lastModified, + modifiedBy: core.account.System, + provider: '', + contentType: fileResult.metadata.contentType, + etag: fileResult.metadata.etag, + version: null, + size: fileResult.metadata.size + } + } else { + const error = (result[0] as BlobUploadError) ?? 'Unknown error' + throw new StorageError(`Storage error ${error.error}`) + } + } + + async partial (objectName: string, offset: number, length?: number): Promise { + const url = this.getObjectUrl(objectName) + + const response = await wrappedFetch(url, { + headers: { + ...this.headers, + Range: length !== undefined ? `bytes=${offset}-${offset + length - 1}` : `bytes=${offset}` + } + }) + + if (response.body == null) { + throw new StorageError('Missing response body') + } + return Readable.from(response.body) + } + + async remove (objectName: string): Promise { + const url = this.getObjectUrl(objectName) + await wrappedFetch(url, { + method: 'DELETE', + headers: { ...this.headers } + }) + } +} + +async function toBuffer (data: Buffer | string | Readable): Promise { + if (Buffer.isBuffer(data)) { + return data + } else if (typeof data === 'string') { + return Buffer.from(data) + } else if (data instanceof Readable) { + const chunks: Buffer[] = [] + for await (const chunk of data) { + chunks.push(chunk) + } + return Buffer.concat(chunks as any) + } else { + throw new TypeError('Unsupported data type') + } +} + +async function wrappedFetch (url: string | URL, init?: RequestInit): Promise { + let response: Response + try { + response = await fetch(url, init) + } catch (error: any) { + throw new NetworkError(`Network error ${error}`) + } + if (!response.ok) { + const text = await response.text() + if (response.status === 404) { + throw new NotFoundError(text) + } else { + throw new StorageError(text) + } + } + return response +} + +export function createStorageClient ( + filesUrl: string, + uploadUrl: string, + token: string, + workspace: WorkspaceUuid +): StorageClient { + return new StorageClientImpl(filesUrl, uploadUrl, token, workspace) +} + +export async function connectStorage (url: string, options: AuthOptions, config?: ServerConfig): Promise { + config ??= await loadServerConfig(url) + const token = await getWorkspaceToken(url, options, config) + const filesUrl = (config.FILES_URL.startsWith('/') ? concatLink(url, config.FILES_URL) : config.FILES_URL).replace( + ':workspace', + token.workspaceId + ) + const uploadUrl = ( + config.UPLOAD_URL.startsWith('/') ? concatLink(url, config.UPLOAD_URL) : config.UPLOAD_URL + ).replace(':workspace', token.workspaceId) + return new StorageClientImpl(filesUrl, uploadUrl, token.token, token.workspaceId) +} diff --git a/foundations/core/packages/api-client/src/storage/error.ts b/foundations/core/packages/api-client/src/storage/error.ts new file mode 100644 index 0000000000..2e5d98590d --- /dev/null +++ b/foundations/core/packages/api-client/src/storage/error.ts @@ -0,0 +1,35 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export class NetworkError extends Error { + constructor (message: string) { + super(message) + this.name = 'NetworkError' + } +} + +export class StorageError extends Error { + constructor (message: string) { + super(message) + this.name = 'StorageError' + } +} + +export class NotFoundError extends StorageError { + constructor (message = 'Not Found') { + super(message) + this.name = 'NotFoundError' + } +} diff --git a/foundations/core/packages/api-client/src/storage/index.ts b/foundations/core/packages/api-client/src/storage/index.ts new file mode 100644 index 0000000000..8be0b038d4 --- /dev/null +++ b/foundations/core/packages/api-client/src/storage/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export { createStorageClient, connectStorage } from './client' +export * from './error' +export * from './types' diff --git a/foundations/core/packages/api-client/src/storage/types.ts b/foundations/core/packages/api-client/src/storage/types.ts new file mode 100644 index 0000000000..a3aed0c22d --- /dev/null +++ b/foundations/core/packages/api-client/src/storage/types.ts @@ -0,0 +1,25 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Blob } from '@hcengineering/core' +import { Readable } from 'stream' + +export interface StorageClient { + stat: (objectName: string) => Promise + get: (objectName: string) => Promise + put: (objectName: string, stream: Readable | Buffer | string, contentType: string, size?: number) => Promise + partial: (objectName: string, offset: number, length?: number) => Promise + remove: (objectName: string) => Promise +} diff --git a/foundations/core/packages/api-client/src/types.ts b/foundations/core/packages/api-client/src/types.ts new file mode 100644 index 0000000000..0b509ceee1 --- /dev/null +++ b/foundations/core/packages/api-client/src/types.ts @@ -0,0 +1,224 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type ClientSocketFactory } from '@hcengineering/client' +import { + CollaborativeDoc, + type Account, + type AttachedData, + type AttachedDoc, + type Class, + type Data, + type Doc, + type DocumentQuery, + type DocumentUpdate, + type FindOptions, + type FindResult, + type Hierarchy, + type Mixin, + type MixinData, + type MixinUpdate, + type ModelDb, + type Ref, + type Space, + type TxResult, + type WithLookup +} from '@hcengineering/core' +import { type MarkupContent, type MarkupOperations } from './markup' + +type WithPropertyType = { + [P in keyof T]: T[P] extends X ? Y : T[P] +} + +/** @public */ +export type WithMarkup = WithPropertyType< +WithPropertyType, +CollaborativeDoc, +MarkupContent +> + +/** + * Platform API client + * @public + * */ +export type PlatformClient = { + getHierarchy: () => Hierarchy + + getModel: () => ModelDb + + getAccount: () => Promise + + close: () => Promise +} & FindOperations & +DocOperations & +CollectionOperations & +MixinOperations & +MarkupOperations & +AsyncDisposable + +/** + * @public + */ +export interface FindOperations { + findAll: ( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions | undefined + ) => Promise> + + findOne: ( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions | undefined + ) => Promise | undefined> +} + +/** + * @public + */ +export interface DocOperations { + createDoc: ( + _class: Ref>, + space: Ref, + attributes: WithMarkup>, + id?: Ref + ) => Promise> + + updateDoc: ( + _class: Ref>, + space: Ref, + objectId: Ref, + operations: WithMarkup>, + retrieve?: boolean + ) => Promise + + removeDoc: (_class: Ref>, space: Ref, objectId: Ref) => Promise +} + +/** + * @public + */ +export interface CollectionOperations { + addCollection: ( + _class: Ref>, + space: Ref, + attachedTo: Ref, + attachedToClass: Ref>, + collection: Extract | string, + attributes: WithMarkup>, + id?: Ref

+ ) => Promise> + + updateCollection: ( + _class: Ref>, + space: Ref, + objectId: Ref

, + attachedTo: Ref, + attachedToClass: Ref>, + collection: Extract | string, + operations: WithMarkup>, + retrieve?: boolean + ) => Promise> + + removeCollection: ( + _class: Ref>, + space: Ref, + objectId: Ref

, + attachedTo: Ref, + attachedToClass: Ref>, + collection: Extract | string + ) => Promise> +} + +/** + * @public + */ +export interface MixinOperations { + createMixin: ( + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + mixin: Ref>, + attributes: WithMarkup> + ) => Promise + + updateMixin: ( + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + mixin: Ref>, + attributes: WithMarkup> + ) => Promise +} + +/** + * Configuration options for password-based authentication + * @public + */ +export interface PasswordAuthOptions { + /** User's email address */ + email: string + + /** User's password */ + password: string + + /** Workspace URL name */ + workspace: string +} + +/** + * Configuration options for token-based authentication + * @public + */ +export interface TokenAuthOptions { + /** Authentication token */ + token: string + + /** Workspace URL name */ + workspace: string +} + +/** + * Union type representing all authentication options + * Can be either password-based or token-based authentication + * @public + */ +export type AuthOptions = PasswordAuthOptions | TokenAuthOptions + +/** + * Configuration options for socket connection + * @public + */ +export interface ConnectSocketOptions { + /** + * Optional factory for creating custom WebSocket implementations + * Particularly useful in Node.js environments where you might need + * to provide a specific WebSocket client implementation + * If not provided, a default WebSocket implementation will be used + */ + socketFactory?: ClientSocketFactory + + /** + * Optional timeout duration for the connection attempt in milliseconds + * Specifies how long to wait for a connection before timing out + */ + connectionTimeout?: number +} + +/** + * API connect options + * @public + */ +export type ConnectOptions = ConnectSocketOptions & AuthOptions diff --git a/foundations/core/packages/api-client/src/utils.ts b/foundations/core/packages/api-client/src/utils.ts new file mode 100644 index 0000000000..0d6efe5f3c --- /dev/null +++ b/foundations/core/packages/api-client/src/utils.ts @@ -0,0 +1,55 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type WorkspaceLoginInfo, getClient as getAccountClient } from '@hcengineering/account-client' +import { WorkspaceUuid } from '@hcengineering/core' +import { AuthOptions } from './types' +import { loadServerConfig, ServerConfig } from './config' + +export interface WorkspaceToken { + endpoint: string + token: string + workspaceId: WorkspaceUuid + info: WorkspaceLoginInfo +} + +export async function getWorkspaceToken ( + url: string, + options: AuthOptions, + config?: ServerConfig +): Promise { + config ??= await loadServerConfig(url) + + let token: string | undefined + + if ('token' in options) { + token = options.token + } else { + const { email, password } = options + const loginInfo = await getAccountClient(config.ACCOUNTS_URL).login(email, password) + token = loginInfo.token + } + + if (token === undefined) { + throw new Error('Login failed') + } + + const ws = await getAccountClient(config.ACCOUNTS_URL, token).selectWorkspace(options.workspace) + if (ws === undefined) { + throw new Error('Workspace not found') + } + + return { endpoint: ws.endpoint, token: ws.token, workspaceId: ws.workspace, info: ws } +} diff --git a/foundations/core/packages/api-client/tsconfig.json b/foundations/core/packages/api-client/tsconfig.json new file mode 100644 index 0000000000..28ecf4dfd3 --- /dev/null +++ b/foundations/core/packages/api-client/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declaration": true, + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo", + "types": ["node", "jest"] + } +} diff --git a/foundations/core/packages/client-resources/.eslintrc.js b/foundations/core/packages/client-resources/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/client-resources/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/client-resources/.npmignore b/foundations/core/packages/client-resources/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/client-resources/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/client-resources/CHANGELOG.json b/foundations/core/packages/client-resources/CHANGELOG.json new file mode 100644 index 0000000000..c9f112b314 --- /dev/null +++ b/foundations/core/packages/client-resources/CHANGELOG.json @@ -0,0 +1,175 @@ +{ + "name": "@hcengineering/client-resources", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/client-resources_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.6", + "tag": "@hcengineering/client-resources_v0.7.6", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/client\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/rpc\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/client-resources_v0.7.5", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/client\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/rpc\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/client-resources_v0.7.4", + "date": "Thu, 09 Oct 2025 16:57:55 GMT", + "comments": { + "patch": [ + { + "comment": "fix formatting" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/client\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/client-resources_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.6.4", + "tag": "@hcengineering/client-resources_v0.6.4", + "date": "Tue, 09 Nov 2021 11:00:07 GMT", + "comments": { + "patch": [ + { + "comment": "Expose `connect`." + } + ] + } + }, + { + "version": "0.6.3", + "tag": "@hcengineering/client-resources_v0.6.3", + "date": "Sat, 14 Aug 2021 09:12:06 GMT", + "comments": { + "patch": [ + { + "comment": "ping" + } + ] + } + }, + { + "version": "0.6.2", + "tag": "@hcengineering/client-resources_v0.6.2", + "date": "Wed, 11 Aug 2021 10:08:19 GMT", + "comments": { + "patch": [ + { + "comment": "server ping" + } + ] + } + }, + { + "version": "0.6.1", + "tag": "@hcengineering/client-resources_v0.6.1", + "date": "Sun, 08 Aug 2021 21:05:26 GMT", + "comments": { + "patch": [ + { + "comment": "Fix server connection" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `~0.6.0` to `~0.6.8`" + }, + { + "comment": "Updating dependency \"@hcengineering/client\" from `~0.6.0` to `~0.6.1`" + } + ] + } + }, + { + "version": "0.6.0", + "tag": "@hcengineering/client-resources_v0.6.0", + "date": "Sun, 08 Aug 2021 10:14:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.3` to `~0.6.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/client-resources/CHANGELOG.md b/foundations/core/packages/client-resources/CHANGELOG.md new file mode 100644 index 0000000000..a15491ef4e --- /dev/null +++ b/foundations/core/packages/client-resources/CHANGELOG.md @@ -0,0 +1,68 @@ +# Change Log - @hcengineering/client-resources + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.6 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.5 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.4 +Thu, 09 Oct 2025 16:57:55 GMT + +### Patches + +- fix formatting + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Version update only_ + +## 0.6.4 +Tue, 09 Nov 2021 11:00:07 GMT + +### Patches + +- Expose `connect`. + +## 0.6.3 +Sat, 14 Aug 2021 09:12:06 GMT + +### Patches + +- ping + +## 0.6.2 +Wed, 11 Aug 2021 10:08:19 GMT + +### Patches + +- server ping + +## 0.6.1 +Sun, 08 Aug 2021 21:05:26 GMT + +### Patches + +- Fix server connection + +## 0.6.0 +Sun, 08 Aug 2021 10:14:57 GMT + +_Initial release_ + diff --git a/foundations/core/packages/client-resources/config/rig.json b/foundations/core/packages/client-resources/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/client-resources/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/client-resources/jest.config.js b/foundations/core/packages/client-resources/jest.config.js new file mode 100644 index 0000000000..fc291ae4b4 --- /dev/null +++ b/foundations/core/packages/client-resources/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'], + testTimeout: 10000, // 10 seconds timeout for all tests + forceExit: true // Force exit after tests complete to handle lingering timers +} diff --git a/foundations/core/packages/client-resources/package.json b/foundations/core/packages/client-resources/package.json new file mode 100644 index 0000000000..6272bb9af9 --- /dev/null +++ b/foundations/core/packages/client-resources/package.json @@ -0,0 +1,65 @@ +{ + "name": "@hcengineering/client-resources", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent --coverage", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/snappyjs": "^0.7.1", + "@types/node": "^22.18.1", + "ws": "^8.18.2", + "@types/ws": "^8.5.12", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/analytics": "workspace:^0.7.17", + "@hcengineering/client": "workspace:^0.7.17", + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/platform": "workspace:^0.7.18", + "@hcengineering/rpc": "workspace:^0.7.17", + "snappyjs": "^0.7.0" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/client-resources/readme.md b/foundations/core/packages/client-resources/readme.md new file mode 100644 index 0000000000..64370999be --- /dev/null +++ b/foundations/core/packages/client-resources/readme.md @@ -0,0 +1,33 @@ +# Overview + +Package allow to create a client to interact with running platform. + +## Usage + +```ts + import clientResources from '@hcengineering/client-resources' + import core, { Client } from '@hcengineering/core' + + // ... + + const token = ... // Token obtained somehow. + + const connection: Client = await (await clientResources()).function.GetClient(token, transactorUrl) + + // Now client is usable + + // Use close, to shutdown connection. + await connection.close() +``` + +## Node JS + +For NodeJS environment it is required to configure ClientSocketFactory using 'ws' package. + +```ts +// We need to override default WebSocket factory with 'ws' one. +setMetadata(client.metadata.ClientSocketFactory, (url) => new WebSocket(url)) + +const connection: Client = await (await clientResources()).function.GetClient(token, transactorUrl) +... +``` diff --git a/foundations/core/packages/client-resources/src/__tests__/connection.test.ts b/foundations/core/packages/client-resources/src/__tests__/connection.test.ts new file mode 100644 index 0000000000..7fb4f195be --- /dev/null +++ b/foundations/core/packages/client-resources/src/__tests__/connection.test.ts @@ -0,0 +1,374 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { ClientSocket, ClientSocketReadyState, pongConst, pingConst } from '@hcengineering/client' +import core, { + type Tx, + generateId, + type WorkspaceUuid, + type PersonUuid, + TxCreateDoc, + type Doc +} from '@hcengineering/core' +import { connect } from '../connection' + +// Mock CloseEvent for Node.js environment (used in MockWebSocket) +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +class MockCloseEvent { + readonly type: string + readonly code: number + + constructor (type: string, init?: { code?: number }) { + this.type = type + this.code = init?.code ?? 1000 + } +} + +// Mock MessageEvent for Node.js environment (used in MockWebSocket) +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +class MockMessageEvent { + readonly type: string + readonly data: any + + constructor (type: string, init: { data: any }) { + this.type = type + this.data = init.data + } +} + +// Mock Event for Node.js environment (used in MockWebSocket) +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +class MockEvent { + readonly type: string + + constructor (type: string) { + this.type = type + } +} + +// Mock WebSocket implementation for testing +class MockWebSocket implements ClientSocket { + readyState: ClientSocketReadyState = ClientSocketReadyState.CONNECTING + onmessage?: ((this: ClientSocket, ev: MessageEvent) => any) | null = null + onclose?: ((this: ClientSocket, ev: CloseEvent) => any) | null = null + onopen?: ((this: ClientSocket, ev: Event) => any) | null = null + onerror?: ((this: ClientSocket, ev: Event) => any) | null = null + bufferedAmount?: number = 0 + + private readonly messageQueue: any[] = [] + private closeCode?: number + private timers: any[] = [] + + constructor (public url: string) { + // Simulate async connection + const timer = setTimeout(() => { + if (this.readyState === ClientSocketReadyState.CLOSED) { + return + } + this.readyState = ClientSocketReadyState.OPEN + if (this.onopen !== null && this.onopen !== undefined) { + this.onopen(new MockEvent('open') as any) + } + // Process queued messages + this.processQueue() + }, 10) + this.timers.push(timer) + } + + clearAllTimers (): void { + for (const timer of this.timers) { + clearTimeout(timer) + } + this.timers = [] + } + + send (data: string | ArrayBufferLike | Blob | ArrayBufferView): void { + if (this.readyState !== ClientSocketReadyState.OPEN) { + throw new Error('WebSocket is not open') + } + + // Parse and respond to messages + try { + const message = typeof data === 'string' ? data : new TextDecoder().decode(data as ArrayBuffer) + + if (message === pingConst) { + this.simulateMessage(pongConst) + return + } + + // Parse RPC message + const rpcMessage = JSON.parse(message) + + // Handle hello request + if (rpcMessage.method === 'hello') { + const response = { + id: rpcMessage.id, + result: { + binary: false, + compression: false, + reconnect: false + } + } + this.simulateMessage(JSON.stringify(response)) + return + } + + // Handle loadModel request + if (rpcMessage.method === 'loadModel') { + const response = { + id: rpcMessage.id, + result: { + transactions: [], + hash: 'test-hash', + full: false + } + } + this.simulateMessage(JSON.stringify(response)) + return + } + + // Handle findAll request + if (rpcMessage.method === 'findAll') { + const response = { + id: rpcMessage.id, + result: [] + } + this.simulateMessage(JSON.stringify(response)) + return + } + + // Handle tx request + if (rpcMessage.method === 'tx') { + const response = { + id: rpcMessage.id, + result: {} + } + this.simulateMessage(JSON.stringify(response)) + return + } + + // Default response + const response = { + id: rpcMessage.id, + result: {} + } + this.simulateMessage(JSON.stringify(response)) + } catch (err) { + console.error('Error processing message:', err) + } + } + + close (code?: number): void { + this.closeCode = code + this.readyState = ClientSocketReadyState.CLOSING + this.clearAllTimers() + const timer = setTimeout(() => { + this.readyState = ClientSocketReadyState.CLOSED + if (this.onclose !== null && this.onclose !== undefined) { + this.onclose(new MockCloseEvent('close', { code: code ?? 1000 }) as any) + } + }, 10) + this.timers.push(timer) + } + + // Helper method to simulate receiving messages + simulateMessage (data: string | ArrayBuffer): void { + if (this.readyState === ClientSocketReadyState.OPEN) { + if (this.onmessage !== null && this.onmessage !== undefined) { + const event = new MockMessageEvent('message', { data }) + this.onmessage(event as any) + } + } else { + this.messageQueue.push(data) + } + } + + private processQueue (): void { + while (this.messageQueue.length > 0) { + const data = this.messageQueue.shift() + this.simulateMessage(data) + } + } + + // Helper to simulate server-initiated transactions + simulateTransaction (tx: Tx): void { + const message = { + result: { + _class: 'core:class:TxNotification', + tx: [tx] + } + } + this.simulateMessage(JSON.stringify(message)) + } +} + +describe('MockWebSocket', () => { + it('should connect and send messages', async () => { + const ws = new MockWebSocket('ws://localhost:3333') + + await new Promise((resolve) => { + ws.onopen = () => { + expect(ws.readyState).toBe(ClientSocketReadyState.OPEN) + resolve() + } + }) + + // Test message sending + let receivedMessage: string | null = null + ws.onmessage = (ev: MessageEvent) => { + receivedMessage = ev.data as string + } + + ws.simulateMessage('test message') + + expect(receivedMessage).toBe('test message') + + // Test close + await new Promise((resolve) => { + ws.onclose = () => { + expect(ws.readyState).toBe(ClientSocketReadyState.CLOSED) + resolve() + } + ws.close() + }) + }) + + it('should handle ping/pong', async () => { + const ws = new MockWebSocket('ws://localhost:3333') + + await new Promise((resolve) => { + ws.onopen = () => { + resolve() + } + }) + + let receivedMessage: string | null = null + ws.onmessage = (ev: MessageEvent) => { + receivedMessage = ev.data as string + } + + ws.send(pingConst) + + await new Promise((resolve) => setTimeout(resolve, 50)) + + expect(receivedMessage).toBe(pongConst) + + ws.close() + }) +}) + +describe('connect function', () => { + let connections: Array<{ close: () => Promise }> = [] + let mockWebSockets: MockWebSocket[] = [] + + afterEach(async () => { + // Clean up all connections + for (const conn of connections) { + await conn.close() + } + connections = [] + + // Clean up all mock websockets + for (const ws of mockWebSockets) { + ws.clearAllTimers() + if (ws.readyState !== ClientSocketReadyState.CLOSED) { + ws.close() + } + } + mockWebSockets = [] + + // Give time for all timers to clear + await new Promise((resolve) => setTimeout(resolve, 100)) + }) + + it('should establish connection', async () => { + const workspaceId = 'test-workspace' as WorkspaceUuid + const userId = 'test-user' as PersonUuid + + const handler = jest.fn() + + const mockWs = new MockWebSocket('ws://localhost:3333') + mockWebSockets.push(mockWs) + + const client = connect('ws://localhost:3333', handler, workspaceId, userId, { + socketFactory: (url: string) => mockWs as any + }) + + connections.push(client) + + expect(client).toBeDefined() + + // Wait for connection to establish + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Close connection immediately to prevent timers from continuing + await client.close() + + // Wait for close to complete + await new Promise((resolve) => setTimeout(resolve, 50)) + }) + + it('should handle transactions', async () => { + const workspaceId = 'test-workspace' as WorkspaceUuid + const userId = 'test-user' as PersonUuid + + let txReceived: Tx | null = null + const handler = (...tx: Tx[]): void => { + if (tx.length > 0) { + txReceived = tx[0] + } + } + + const mockWs = new MockWebSocket('ws://localhost:3333') + const client = connect('ws://localhost:3333', handler, workspaceId, userId, { + socketFactory: (url: string) => mockWs as any + }) + + connections.push(client) + + // Wait for connection to establish + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Simulate a transaction from server + const testTx: TxCreateDoc = { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Model, + objectId: generateId(), + objectClass: core.class.Space, + objectSpace: core.space.Model, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + createdOn: Date.now(), + attributes: { + name: 'Test Space', + description: '', + private: false, + archived: false, + members: [] + } + } + + mockWs.simulateTransaction(testTx) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(txReceived).toBeDefined() + + // Close immediately after test + await client.close() + await new Promise((resolve) => setTimeout(resolve, 50)) + }) +}) diff --git a/foundations/core/packages/client-resources/src/__tests__/integration.test.ts b/foundations/core/packages/client-resources/src/__tests__/integration.test.ts new file mode 100644 index 0000000000..c0f6b2642e --- /dev/null +++ b/foundations/core/packages/client-resources/src/__tests__/integration.test.ts @@ -0,0 +1,537 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { + type Client, + type Tx, + generateId, + TxCreateDoc, + type Doc, + createClient, + type ClientConnection, + type TxHandler, + ClientConnectEvent, + type LoadModelResponse, + Hierarchy, + ModelDb, + TxDb, + type Class, + type Ref, + type DocumentQuery, + type FindOptions, + type FindResult, + type SearchQuery, + type SearchOptions, + type SearchResult, + type TxResult, + type Timestamp, + type Domain, + type DocChunk, + type OperationDomain, + type DomainParams, + type DomainRequestOptions, + type DomainResult, + DOMAIN_MODEL, + DOMAIN_TX, + ClassifierKind, + TxFactory, + type Data, + type Obj +} from '@hcengineering/core' +import type { IntlString } from '@hcengineering/platform' + +const txFactory = new TxFactory(core.account.System) + +function createClass (_class: Ref>, attributes: Data>): TxCreateDoc { + return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class) +} + +// Generate minimal model for testing +function generateMinimalModel (): Tx[] { + const txes: Tx[] = [] + + txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS })) + txes.push( + createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS }) + ) + txes.push( + createClass(core.class.Class, { + label: 'Class' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + txes.push( + createClass(core.class.Space, { + label: 'Space' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + txes.push( + createClass(core.class.Tx, { + label: 'Tx' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TX + }) + ) + txes.push( + createClass(core.class.TxCUD, { + label: 'TxCUD' as IntlString, + extends: core.class.Tx, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TX + }) + ) + txes.push( + createClass(core.class.TxCreateDoc, { + label: 'TxCreateDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxUpdateDoc, { + label: 'TxUpdateDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxRemoveDoc, { + label: 'TxRemoveDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + + return txes +} + +// Test utilities for creating mock servers and clients + +/** + * Mock ClientConnection for integration testing + */ +export class MockClientConnection implements ClientConnection { + private readonly handlers: TxHandler[] = [] + private _connected: boolean = true + private readonly hierarchy: Hierarchy + private readonly model: ModelDb + private readonly transactions: TxDb + + onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise + getLastHash?: () => Promise + + constructor (txes: Tx[] = []) { + const minimalModel = txes.length === 0 ? generateMinimalModel() : txes + + this.hierarchy = new Hierarchy() + for (const tx of minimalModel) { + this.hierarchy.tx(tx) + } + + this.model = new ModelDb(this.hierarchy) + this.transactions = new TxDb(this.hierarchy) + + for (const tx of minimalModel) { + void this.model.tx(tx) + void this.transactions.tx(tx) + } + } + + isConnected (): boolean { + return this._connected + } + + setConnected (value: boolean): void { + this._connected = value + } + + pushHandler (handler: TxHandler): void { + this.handlers.push(handler) + } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + return await this.model.findAll(_class, query, options) + } + + async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { + return { docs: [] } + } + + async tx (tx: Tx): Promise { + if (tx.objectSpace === core.space.Model) { + this.hierarchy.tx(tx) + await this.model.tx(tx) + } + await this.transactions.tx(tx) + + this.handlers.forEach((h) => { + h(tx) + }) + + return {} + } + + async loadModel (last: Timestamp, hash?: string): Promise { + const txes = await this.transactions.findAll(core.class.Tx, { + objectSpace: core.space.Model, + modifiedOn: { $gt: last } + }) + + if (hash !== undefined) { + return { + transactions: txes, + hash: 'test-hash', + full: false + } + } + return txes + } + + async close (): Promise { + this._connected = false + } + + async loadChunk (domain: Domain, idx?: number): Promise { + return { + idx: idx ?? 0, + docs: [], + finished: true + } + } + + async getDomainHash (domain: Domain): Promise { + return 'test-hash' + } + + async closeChunk (idx: number): Promise {} + + async loadDocs (domain: Domain, docs: Ref[]): Promise { + return [] + } + + async upload (domain: Domain, docs: Doc[]): Promise {} + + async clean (domain: Domain, docs: Ref[]): Promise {} + + async sendForceClose (): Promise {} + + async domainRequest ( + ctx: OperationDomain, + params: DomainParams, + options?: DomainRequestOptions + ): Promise { + return { domain: ctx, value: null } + } + + simulateTransaction (tx: Tx): void { + this.handlers.forEach((h) => { + h(tx) + }) + } + + async simulateConnect (event: ClientConnectEvent = ClientConnectEvent.Connected): Promise { + if (this.onConnect !== undefined) { + await this.onConnect(event, undefined, {}) + } + } +} + +/** + * Create a test client with mock connection + */ +export async function createTestClient (initialTxes: Tx[] = []): Promise<{ + client: Client + connection: MockClientConnection + close: () => Promise +}> { + const connection = new MockClientConnection(initialTxes) + + const client = await createClient(async (handler: TxHandler) => { + connection.pushHandler(handler) + return connection + }) + + return { + client, + connection, + close: async () => { + await client.close() + } + } +} + +/** + * Helper to create a test transaction + */ +export function createTestTx (objectClass: Ref> = core.class.Space, attributes: any = {}): TxCreateDoc { + return { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + objectId: generateId(), + objectClass, + objectSpace: core.space.Model, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + createdOn: Date.now(), + attributes: { + name: 'TestDoc', + description: '', + ...attributes + } + } +} + +describe('Client-Resources Integration Tests', () => { + it('should create a full client with connection', async () => { + const { client, connection, close } = await createTestClient() + + expect(client).toBeDefined() + expect(client.getHierarchy()).toBeDefined() + expect(client.getModel()).toBeDefined() + expect(connection.isConnected()).toBe(true) + + await close() + }) + + it('should handle transactions end-to-end', async () => { + const { client, close } = await createTestClient() + + const notifySpy = jest.fn() + client.notify = notifySpy + + const tx = createTestTx(core.class.Space, { + name: 'TestSpace', + private: false, + archived: false, + members: [] + }) + + // When we tx directly, it goes through the connection which simulates the server + // The transaction is processed locally first, then notify is called when + // transaction comes back from "server" (our mock connection) + await client.tx(tx) + + // The mock connection immediately notifies handlers when we call tx + // So notifySpy should have been called + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(notifySpy).toHaveBeenCalled() + + await close() + }) + + it('should handle reconnection events', async () => { + const { connection, close } = await createTestClient() + + let connectEventReceived: ClientConnectEvent | undefined + + const originalOnConnect = connection.onConnect + connection.onConnect = async (event, lastTx, data) => { + connectEventReceived = event + await originalOnConnect?.(event, lastTx, data) + } + + await connection.simulateConnect(ClientConnectEvent.Reconnected) + + expect(connectEventReceived).toBe(ClientConnectEvent.Reconnected) + + await close() + }) + + it('should handle model updates', async () => { + const { client, close } = await createTestClient() + + const tx = createTestTx(core.class.Space, { + name: 'ModelSpace', + private: false, + archived: false, + members: [] + }) + + tx.objectSpace = core.space.Model + + await client.tx(tx) + + const spaces = await client.findAll(core.class.Space, { name: 'ModelSpace' }) + expect(spaces).toBeDefined() + + await close() + }) + + it('should handle findAll with options', async () => { + const { client, close } = await createTestClient() + + const result = await client.findAll(core.class.Space, {}, { limit: 10, sort: { name: 1 } }) + + expect(result).toBeDefined() + expect(Array.isArray(result)).toBe(true) + expect(result.total).toBeDefined() + + await close() + }) + + it('should handle findOne', async () => { + const { client, close } = await createTestClient() + + const result = await client.findOne(core.class.Space, {}) + + // Result may be undefined if no documents exist + expect(result === undefined || result !== null).toBe(true) + + await close() + }) + + it('should handle connection loss and reconnection', async () => { + const { connection, close } = await createTestClient() + + expect(connection.isConnected()).toBe(true) + + connection.setConnected(false) + expect(connection.isConnected()).toBe(false) + + connection.setConnected(true) + expect(connection.isConnected()).toBe(true) + + await close() + }) + + it('should handle multiple handlers', async () => { + const { connection, close } = await createTestClient() + + const handler1 = jest.fn() + const handler2 = jest.fn() + + connection.pushHandler(handler1) + connection.pushHandler(handler2) + + const tx = createTestTx() + + connection.simulateTransaction(tx) + + expect(handler1).toHaveBeenCalledWith(tx) + expect(handler2).toHaveBeenCalledWith(tx) + + await close() + }) + + it('should handle searchFulltext', async () => { + const { client, close } = await createTestClient() + + const query: SearchQuery = { query: 'test' } + const options: SearchOptions = {} + + const result = await client.searchFulltext(query, options) + + expect(result).toBeDefined() + expect(result.docs).toBeDefined() + expect(Array.isArray(result.docs)).toBe(true) + + await close() + }) + + it('should handle domainRequest', async () => { + const { client, close } = await createTestClient() + + const result = await client.domainRequest('test-domain' as OperationDomain, {}) + + expect(result).toBeDefined() + + await close() + }) + + it('should properly clean up on close', async () => { + const { connection, close } = await createTestClient() + + expect(connection.isConnected()).toBe(true) + + await close() + + expect(connection.isConnected()).toBe(false) + }) + + it('should handle upgrade events', async () => { + const { connection, close } = await createTestClient() + + let upgradeReceived = false + + const originalOnConnect = connection.onConnect + connection.onConnect = async (event, lastTx, data) => { + if (event === ClientConnectEvent.Upgraded) { + upgradeReceived = true + } + await originalOnConnect?.(event, lastTx, data) + } + + await connection.simulateConnect(ClientConnectEvent.Upgraded) + + expect(upgradeReceived).toBe(true) + + await close() + }) + + it('should handle maintenance events', async () => { + const { connection, close } = await createTestClient() + + let maintenanceReceived = false + + const originalOnConnect = connection.onConnect + connection.onConnect = async (event, lastTx, data) => { + if (event === ClientConnectEvent.Maintenance) { + maintenanceReceived = true + } + await originalOnConnect?.(event, lastTx, data) + } + + await connection.simulateConnect(ClientConnectEvent.Maintenance) + + expect(maintenanceReceived).toBe(true) + + await close() + }) + + it('should handle concurrent transactions', async () => { + const { client, connection, close } = await createTestClient() + + const notifySpy = jest.fn() + client.notify = notifySpy + + const tx1 = createTestTx(core.class.Space, { name: 'Space1' }) + const tx2 = createTestTx(core.class.Space, { name: 'Space2' }) + const tx3 = createTestTx(core.class.Space, { name: 'Space3' }) + + await Promise.all([client.tx(tx1), client.tx(tx2), client.tx(tx3)]) + + connection.simulateTransaction(tx1) + connection.simulateTransaction(tx2) + connection.simulateTransaction(tx3) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(notifySpy.mock.calls.length).toBeGreaterThanOrEqual(3) + + await close() + }) +}) diff --git a/foundations/core/packages/client-resources/src/connection.ts b/foundations/core/packages/client-resources/src/connection.ts new file mode 100644 index 0000000000..b692c2ccc7 --- /dev/null +++ b/foundations/core/packages/client-resources/src/connection.ts @@ -0,0 +1,911 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Analytics } from '@hcengineering/analytics' +import client, { + type ClientFactoryOptions, + ClientSocket, + ClientSocketReadyState, + pingConst, + pongConst +} from '@hcengineering/client' +import core, { + Account, + Class, + ClientConnectEvent, + ClientConnection, + clone, + Doc, + DocChunk, + DocumentQuery, + Domain, + type DomainParams, + type DomainRequestOptions, + type DomainResult, + FindOptions, + FindResult, + generateId, + LoadModelResponse, + type MeasureContext, + MeasureMetricsContext, + type OperationDomain, + type PersonUuid, + Ref, + SearchOptions, + SearchQuery, + SearchResult, + Timestamp, + toFindResult, + Tx, + TxApplyIf, + TxHandler, + TxResult, + type WorkspaceUuid +} from '@hcengineering/core' +import platform, { getMetadata, PlatformError, Severity, Status, UNAUTHORIZED } from '@hcengineering/platform' +import { HelloRequest, HelloResponse, type RateLimitInfo, ReqId, type Response, RPCHandler } from '@hcengineering/rpc' +import { uncompress } from 'snappyjs' + +const SECOND = 1000 +const pingTimeout = 10 * SECOND +const hangTimeout = 5 * 60 * SECOND +const dialTimeout = 30 * SECOND + +class RequestPromise { + startTime: number = Date.now() + handleTime?: (diff: number, result: any, serverTime: number, queue: number, toRecieve: number) => void + readonly promise: Promise + resolve!: (value?: any) => void + reject!: (reason?: any) => void + reconnect?: () => void + + // Required to properly handle rate limits + sendData: () => void = () => {} + + constructor ( + readonly method: string, + readonly params: any[], + + readonly handleResult?: (result: any) => Promise + ) { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve + this.reject = reject + }) + } + + chunks?: { index: number, data: FindResult }[] +} + +const globalRPCHandler: RPCHandler = new RPCHandler() + +interface OnConnectHandler { + resolve: () => void + reject: (err: Error) => void +} + +class Connection implements ClientConnection { + private websocket: ClientSocket | null = null + binaryMode = false + compressionMode = false + private readonly requests = new Map() + private lastId = 0 + private interval: number | undefined + private dialTimer: number | undefined + + private sockets = 0 + private openAction: any + + private readonly sessionId: string | undefined + private closed = false + + private upgrading: boolean = false + + private pingResponse: number = Date.now() + + private helloReceived: boolean = false + + private account: Account | undefined + + onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise + + rpcHandler: RPCHandler + + lastHash?: string + + handlers: TxHandler[] = [] + + constructor ( + private readonly ctx: MeasureContext, + private readonly url: string, + handler: TxHandler, + readonly workspace: WorkspaceUuid, + readonly user: PersonUuid, + readonly opt?: ClientFactoryOptions + ) { + if (typeof sessionStorage !== 'undefined') { + // Find local session id in session storage only if user refresh a page. + const sKey = 'session.id.' + this.url + let sessionId = sessionStorage.getItem(sKey) ?? undefined + if (sessionId === undefined) { + sessionId = generateId() + console.log('Generate new SessionId', sessionId) + this.sessionId = sessionId + } else { + this.sessionId = sessionId + sessionStorage.removeItem(sKey) + } + window.addEventListener('beforeunload', () => { + sessionStorage.setItem(sKey, sessionId) + }) + } else { + this.sessionId = generateId() + } + this.rpcHandler = opt?.useGlobalRPCHandler === true ? globalRPCHandler : new RPCHandler() + this.pushHandler(handler) + this.onConnect = opt?.onConnect + + this.scheduleOpen(this.ctx, false) + } + + pushHandler (handler: TxHandler): void { + this.handlers.push(handler) + } + + async getLastHash (ctx: MeasureContext): Promise { + await this.waitOpenConnection(ctx) + return this.lastHash + } + + private schedulePing (socketId: number): void { + this.pingResponse = Date.now() + const wsocket = this.websocket + + clearInterval(this.interval) + this.interval = setInterval(() => { + if (wsocket !== this.websocket) { + clearInterval(this.interval) + return + } + if (!this.upgrading && this.pingResponse !== 0 && Date.now() - this.pingResponse > hangTimeout) { + // No ping response from server. + + if (this.websocket !== null) { + console.log('no ping response from server. Closing socket.', socketId, this.workspace, this.user) + clearInterval(this.interval) + this.websocket.close(1000) + return + } + } + + if (!this.closed) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + void this.sendRequest({ + method: pingConst, + params: [], + once: true, + handleResult: async (result) => { + if (this.websocket === wsocket) { + this.pingResponse = Date.now() + } + } + }).catch((err) => { + this.ctx.error('failed to send msg', { err }) + }) + } else { + clearInterval(this.interval) + } + }, pingTimeout) + } + + async close (): Promise { + this.closed = true + clearTimeout(this.openAction) + clearTimeout(this.dialTimer) + clearInterval(this.interval) + for (const handler of this.onConnectHandlers) { + handler.reject(new Error('Connection closed')) + } + for (const req of this.requests.values()) { + req.reject(new Error('Connection closed')) + } + if (this.websocket !== null) { + this.websocket.close(1000) + this.websocket = null + } + } + + isConnected (): boolean { + return this.websocket != null && this.websocket.readyState === ClientSocketReadyState.OPEN && this.helloReceived + } + + delay = 0 + onConnectHandlers: OnConnectHandler[] = [] + + private waitOpenConnection (ctx: MeasureContext): Promise | undefined { + if (this.isConnected()) { + return undefined + } + + return ctx.with( + 'wait-connection', + {}, + (ctx) => + new Promise((resolve, reject) => { + this.onConnectHandlers.push({ + resolve, + reject + }) + // Websocket is null for first time + this.scheduleOpen(ctx, false) + }) + ) + } + + scheduleOpen (ctx: MeasureContext, force: boolean): void { + if (force) { + ctx.withSync('close-ws', {}, () => { + if (this.websocket !== null) { + this.websocket.close() + this.websocket = null + } + }) + clearTimeout(this.openAction) + this.openAction = undefined + } + clearInterval(this.interval) + if (!this.closed && this.openAction === undefined) { + if (this.websocket === null) { + const socketId = ++this.sockets + // Re create socket in case of error, if not closed + if (this.delay === 0) { + this.openConnection(ctx, socketId) + } else { + this.openAction = setTimeout(() => { + this.openAction = undefined + this.openConnection(ctx, socketId) + }, this.delay * 1000) + } + } + } + } + + currentRateLimit: RateLimitInfo | undefined + slowDownTimer = 0 + + handleMsg (socketId: number, resp: Response): void { + if (this.closed) { + return + } + + if (resp.rateLimit !== undefined && resp.rateLimit.remaining < 50) { + this.currentRateLimit = resp.rateLimit + if (this.currentRateLimit.remaining < this.currentRateLimit.limit / 3) { + if (this.slowDownTimer < 50) { + this.slowDownTimer += 50 + } + this.slowDownTimer++ + } else if (this.slowDownTimer > 0) { + this.slowDownTimer-- + } + } + + if (resp.error !== undefined) { + if (resp.error?.code === UNAUTHORIZED.code || resp.terminate === true) { + if ( + resp.error.code !== platform.status.WorkspaceArchived && + resp.error.code !== platform.status.WorkspaceNotFound + ) { + Analytics.handleError(new PlatformError(resp.error)) + } + this.closed = true + this.websocket?.close() + if (resp.error?.code === UNAUTHORIZED.code) { + this.opt?.onUnauthorized?.() + } + if (resp.error?.code === platform.status.WorkspaceArchived) { + this.opt?.onArchived?.() + } + if (resp.error?.code === platform.status.WorkspaceMigration) { + this.opt?.onMigration?.() + } + } + + if (resp.id !== undefined) { + const promise = this.requests.get(resp.id) + + // Support rate limits + if (resp.rateLimit !== undefined) { + const { remaining, retryAfter } = resp.rateLimit + if (remaining === 0) { + void new Promise((resolve) => setTimeout(resolve, retryAfter ?? 1)).then(() => { + // Retry after a while, so rate limits allow to call more. + promise?.sendData() + }) + return + } + } + + if (promise !== undefined) { + promise.reject(new PlatformError(resp.error)) + } + } + + return + } + + if (resp.id === -1) { + this.delay = 0 + if (resp.result?.state === 'upgrading') { + void this.onConnect?.(ClientConnectEvent.Maintenance, undefined, resp.result.stats) + this.upgrading = true + this.delay = 3 + return + } + if (resp.result === 'hello') { + const helloResp = resp as HelloResponse + this.binaryMode = helloResp.binary + this.compressionMode = helloResp.useCompression ?? false + + // We need to clear dial timer, since we recieve hello response. + clearTimeout(this.dialTimer) + this.dialTimer = undefined + this.lastHash = (resp as HelloResponse).lastHash + + const serverVersion = helloResp.serverVersion + if (typeof window !== 'undefined') { + console.log('Connected to server:', serverVersion) + } + + if (this.opt?.onHello !== undefined && !this.opt.onHello(serverVersion)) { + this.closed = true + this.websocket?.close() + return + } + this.account = helloResp.account + this.helloReceived = true + if (this.upgrading) { + // We need to call upgrade since connection is upgraded + this.opt?.onUpgrade?.() + } + + this.upgrading = false + // Notify all waiting connection listeners + const handlers = this.onConnectHandlers.splice(0, this.onConnectHandlers.length) + for (const h of handlers) { + h.resolve() + } + + for (const [, v] of this.requests.entries()) { + v.reconnect?.() + } + + void this.onConnect?.( + helloResp.reconnect === true ? ClientConnectEvent.Reconnected : ClientConnectEvent.Connected, + helloResp.lastTx, + this.sessionId + )?.catch((err) => { + this.ctx.error('failed to call onConnect', { err }) + }) + this.schedulePing(socketId) + return + } else { + Analytics.handleError(new Error(`unexpected response: ${JSON.stringify(resp)}`)) + } + return + } + if (resp.result === pingConst) { + void this.sendRequest({ method: pingConst, params: [] }).catch((err) => { + this.ctx.error('failed to send ping', { err }) + }) + return + } + if (resp.id !== undefined) { + const promise = this.requests.get(resp.id) + + if (promise === undefined) { + console.error( + new Error(`unknown response id: ${resp.id as string} ${this.workspace} ${this.user}`), + JSON.stringify(this.requests) + ) + return + } + + if (resp.chunk !== undefined) { + promise.chunks = [ + ...(promise.chunks ?? []), + { + index: resp.chunk.index, + data: resp.result as FindResult + } + ] + // console.log(socketId, 'chunk', promise.method, promise.params, promise.chunks.length, (resp.result as []).length) + if (resp.chunk.final) { + promise.chunks.sort((a, b) => a.index - b.index) + let result: any[] = [] + let total = -1 + let lookupMap: Record | undefined + + for (const c of promise.chunks) { + if (c.data.total !== 0) { + total = c.data.total + } + if (c.data.lookupMap !== undefined) { + lookupMap = c.data.lookupMap + } + result = result.concat(c.data) + } + resp.result = toFindResult(result, total, lookupMap) + resp.chunk = undefined + } else { + // Not all chunks are available yet. + return + } + } + + const request = this.requests.get(resp.id) + promise.handleTime?.( + Date.now() - promise.startTime, + resp.result, + resp.time ?? 0, + resp.queue ?? 0, + Date.now() - (resp.bfst ?? 0) + ) + this.requests.delete(resp.id) + if (resp.error !== undefined) { + console.log( + 'ERROR', + 'request:', + request?.method, + 'response-id:', + resp.id, + 'error: ', + resp.error, + 'result: ', + resp.result, + this.workspace, + this.user + ) + promise.reject(new PlatformError(resp.error)) + } else { + if (request?.handleResult !== undefined) { + void request + .handleResult(resp.result) + .then(() => { + promise.resolve(resp.result) + }) + .catch((err) => { + this.ctx.error('failed to handleResult', { err }) + }) + } else { + promise.resolve(resp.result) + } + } + } else { + const txArr = Array.isArray(resp.result) ? (resp.result as Tx[]) : [resp.result as Tx] + + for (const tx of txArr) { + if (tx?._class === core.class.TxModelUpgrade) { + console.log('Processing upgrade', this.workspace, this.user) + this.opt?.onUpgrade?.() + return + } + } + this.handlers.forEach((handler) => { + handler(...txArr) + }) + } + } + + checkArrayBufferPing (data: ArrayBuffer): boolean { + if (data.byteLength === pingConst.length || data.byteLength === pongConst.length) { + const text = new TextDecoder().decode(data) + if (text === pingConst) { + void this.sendRequest({ method: pingConst, params: [] }).catch((err) => { + this.ctx.error('failed to send ping', { err }) + }) + return true + } + if (text === pongConst) { + this.pingResponse = Date.now() + return true + } + } + return false + } + + private openConnection (ctx: MeasureContext, socketId: number): void { + this.binaryMode = false + this.helloReceived = false + // Use defined factory or browser default one. + const clientSocketFactory = + this.opt?.socketFactory ?? + getMetadata(client.metadata.ClientSocketFactory) ?? + ((url: string) => { + const s = new WebSocket(url) + // s.binaryType = 'arraybuffer' + return s as ClientSocket + }) + + if (socketId !== this.sockets) { + return + } + const wsocket = ctx.withSync('create-socket', {}, () => + clientSocketFactory(this.url + `?sessionId=${this.sessionId}`) + ) + + if (socketId !== this.sockets) { + wsocket.close() + return + } + this.websocket = wsocket + if (this.dialTimer === undefined) { + this.dialTimer = setTimeout(() => { + this.dialTimer = undefined + if (!this.closed) { + void this.opt?.onDialTimeout?.()?.catch((err) => { + this.ctx.error('failed to handle dial timeout', { err }) + }) + this.scheduleOpen(this.ctx, true) + } + }, dialTimeout) + } + + wsocket.onmessage = (event: MessageEvent) => { + if (this.closed) { + return + } + if (this.websocket !== wsocket) { + return + } + if (event.data === pongConst) { + this.pingResponse = Date.now() + return + } + if (event.data === pingConst) { + void this.sendRequest({ method: pingConst, params: [] }).catch((err) => { + this.ctx.error('failed to send ping', { err }) + }) + return + } + if (event.data instanceof ArrayBuffer && this.checkArrayBufferPing(event.data)) { + return + } + if (event.data instanceof Blob) { + void event.data + .arrayBuffer() + .then((data) => { + if (this.checkArrayBufferPing(data)) { + // Support ping/pong + return + } + if (this.compressionMode && this.helloReceived) { + try { + data = uncompress(data) + } catch (err: any) { + // Ignore + console.error(err) + } + } + try { + const resp = this.rpcHandler.readResponse(data, this.binaryMode) + this.handleMsg(socketId, resp) + } catch (err: any) { + if (!this.helloReceived) { + // Just error and ignore for now. + console.error(err) + } else { + throw err + } + } + }) + .catch((err) => { + this.ctx.error('failed to decode array buffer', { err }) + }) + } else { + let data = event.data + if (this.compressionMode && this.helloReceived) { + try { + data = uncompress(data) + } catch (err: any) { + // Ignore + console.error(err) + } + } + try { + const resp = this.rpcHandler.readResponse(data, this.binaryMode) + this.handleMsg(socketId, resp) + } catch (err: any) { + if (!this.helloReceived) { + // Just error and ignore for now. + console.error(err) + } else { + throw err + } + } + } + } + wsocket.onclose = (ev) => { + if (this.websocket !== wsocket) { + wsocket.close() + return + } + this.scheduleOpen(this.ctx, true) + } + wsocket.onopen = () => { + if (this.websocket !== wsocket) { + return + } + const useBinary = this.opt?.useBinaryProtocol ?? getMetadata(client.metadata.UseBinaryProtocol) ?? true + this.compressionMode = + this.opt?.useProtocolCompression ?? getMetadata(client.metadata.UseProtocolCompression) ?? false + const helloRequest: HelloRequest = { + method: 'hello', + params: [], + id: -1, + binary: useBinary, + compression: this.compressionMode + } + ctx.withSync('send-hello', {}, () => this.websocket?.send(this.rpcHandler.serialize(helloRequest, false))) + } + + // FIX: remove undefined variable 'opened' + wsocket.onerror = () => { + if (this.websocket !== wsocket) { + return + } + if (this.delay < 3) { + this.delay += 1 + } + console.error('client websocket error:', socketId, this.url, this.workspace, this.user) + } + } + + private sendRequest (data: { + method: string + params: any[] + // If not defined, on reconnect with timeout, will retry automatically. + retry?: () => Promise + handleResult?: (result: any) => Promise + once?: boolean // Require handleResult to retrieve result + measure?: (time: number, result: any, serverTime: number, queue: number, toRecieve: number) => void + allowReconnect?: boolean + overrideId?: number + }): Promise { + return this.ctx.with( + 'send-request', + {}, + async (ctx) => { + if (this.closed) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.ConnectionClosed, {})) + } + + if (this.slowDownTimer > 0) { + // We need to wait a bit to avoid ban. + await new Promise((resolve) => setTimeout(resolve, this.slowDownTimer)) + } + + if (data.once === true) { + // Check if has same request already then skip + const dparams = JSON.stringify(data.params) + for (const [, v] of this.requests) { + if (v.method === data.method && JSON.stringify(v.params) === dparams) { + // We have same unanswered, do not add one more. + return + } + } + } + + const id = data.overrideId ?? this.lastId++ + const promise = new RequestPromise(data.method, data.params, data.handleResult) + promise.handleTime = data.measure + + const w = this.waitOpenConnection(ctx) + if (w instanceof Promise) { + await w + } + if (data.method !== pingConst) { + this.requests.set(id, promise) + } + promise.sendData = (): void => { + if (this.websocket?.readyState === ClientSocketReadyState.OPEN) { + promise.startTime = Date.now() + + if (data.method !== pingConst) { + const dta = this.rpcHandler.serialize( + { + method: data.method, + params: data.params, + meta: ctx.extractMeta(), + id, + time: Date.now() + }, + this.binaryMode + ) + + this.websocket?.send(dta) + } else { + this.websocket?.send(pingConst) + } + } + } + if (data.allowReconnect ?? true) { + promise.reconnect = () => { + setTimeout(async () => { + // In case we don't have response yet. + if (this.requests.has(id) && ((await data.retry?.()) ?? true)) { + promise.sendData() + } + }, 50) + } + } + promise.sendData() + if (data.method !== pingConst) { + return await promise.promise + } + }, + { method: data.method }, + { + span: 'skip' + } + ) + } + + loadModel (last: Timestamp, hash?: string): Promise { + return this.sendRequest({ method: 'loadModel', params: [last, hash] }) + } + + getAccount (): Promise { + if (this.account !== undefined) { + return Promise.resolve(clone(this.account)) + } + return this.sendRequest({ method: 'getAccount', params: [] }) + } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + const result = await this.sendRequest({ + method: 'findAll', + params: [_class, query, options], + measure: (time, result, serverTime, queue, toReceive) => { + if (typeof window !== 'undefined' && (time > 1000 || serverTime > 500)) { + console.error( + 'measure slow findAll', + time, + serverTime, + toReceive, + queue, + _class, + query, + options, + result, + JSON.stringify(result).length + ) + } + } + }) + if (result.lookupMap !== undefined) { + // We need to extract lookup map to document lookups + for (const d of result) { + if (d.$lookup !== undefined) { + for (const [k, v] of Object.entries(d.$lookup)) { + if (!Array.isArray(v)) { + d.$lookup[k] = result.lookupMap[v as any] + } else { + d.$lookup[k] = v.map((it) => result.lookupMap?.[it]) + } + } + } + } + delete result.lookupMap + } + + // We need to revert deleted query simple values. + // We need to get rid of simple query parameters matched in documents + for (const doc of result) { + for (const [k, v] of Object.entries(query)) { + if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') { + if (doc[k] == null) { + doc[k] = v + } + } + } + if (doc._class == null) { + doc._class = _class + } + } + + return result + } + + tx (tx: Tx): Promise { + return this.sendRequest({ + method: 'tx', + params: [tx], + retry: async () => { + if (tx._class === core.class.TxApplyIf) { + return (await this.findAll(core.class.Tx, { _id: (tx as TxApplyIf).txes[0]._id }, { limit: 1 })).length === 0 + } + return (await this.findAll(core.class.Tx, { _id: tx._id }, { limit: 1 })).length === 0 + } + }) + } + + loadChunk (domain: Domain, idx?: number): Promise { + return this.sendRequest({ method: 'loadChunk', params: [domain, idx] }) + } + + async getDomainHash (domain: Domain): Promise { + return await this.sendRequest({ method: 'getDomainHash', params: [domain] }) + } + + closeChunk (idx: number): Promise { + return this.sendRequest({ method: 'closeChunk', params: [idx] }) + } + + loadDocs (domain: Domain, docs: Ref[]): Promise { + return this.sendRequest({ method: 'loadDocs', params: [domain, docs] }) + } + + upload (domain: Domain, docs: Doc[]): Promise { + return this.sendRequest({ method: 'upload', params: [domain, docs] }) + } + + clean (domain: Domain, docs: Ref[]): Promise { + return this.sendRequest({ method: 'clean', params: [domain, docs] }) + } + + searchFulltext (query: SearchQuery, options: SearchOptions): Promise { + return this.sendRequest({ method: 'searchFulltext', params: [query, options] }) + } + + domainRequest (domain: OperationDomain, params: DomainParams, options?: DomainRequestOptions): Promise { + return this.sendRequest({ + method: 'domainRequest', + params: [domain, params], + retry: async () => { + return options?.retry ?? false + } + }) + } + + sendForceClose (): Promise { + return this.sendRequest({ method: 'forceClose', params: [], allowReconnect: false, overrideId: -2, once: true }) + } +} + +/** + * @public + */ +export function connect ( + url: string, + handler: TxHandler, + workspace: WorkspaceUuid, + user: PersonUuid, + opt?: ClientFactoryOptions +): ClientConnection { + return new Connection( + opt?.ctx?.newChild?.('connection', {}) ?? new MeasureMetricsContext('connection', {}), + url, + handler, + workspace, + user, + opt + ) +} diff --git a/foundations/core/packages/client-resources/src/index.ts b/foundations/core/packages/client-resources/src/index.ts new file mode 100644 index 0000000000..9d126e8987 --- /dev/null +++ b/foundations/core/packages/client-resources/src/index.ts @@ -0,0 +1,305 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import clientPlugin from '@hcengineering/client' +import type { ClientFactoryOptions } from '@hcengineering/client/src' +import core, { + Client, + LoadModelResponse, + type PersonUuid, + Tx, + TxHandler, + TxPersistenceStore, + TxWorkspaceEvent, + WorkspaceEvent, + type WorkspaceUuid, + concatLink, + createClient, + fillConfiguration, + pluginFilterTx, + type Class, + type ClientConnection, + type Doc, + type ModelFilter, + type PluginConfiguration, + type Ref, + type TxCUD, + platformNow, + ClientConnectEvent +} from '@hcengineering/core' +import platform, { Severity, Status, getMetadata, getPlugins, setPlatformStatus } from '@hcengineering/platform' +import { connect } from './connection' + +export { connect } + +let dbRequest: IDBOpenDBRequest | undefined +let dbPromise: Promise = Promise.resolve(undefined) + +if (typeof localStorage !== 'undefined') { + const st = platformNow() + dbPromise = new Promise((resolve) => { + dbRequest = indexedDB.open('model.db.persistence', 2) + + dbRequest.onupgradeneeded = function () { + const db = (dbRequest as IDBOpenDBRequest).result + if (!db.objectStoreNames.contains('model')) { + db.createObjectStore('model', { keyPath: 'id' }) + } + } + dbRequest.onsuccess = function () { + const db = (dbRequest as IDBOpenDBRequest).result + console.log('init DB complete', platformNow() - st) + resolve(db) + } + }) + void dbPromise.then((res) => { + if (res !== undefined) { + res.onclose = () => { + dbRequest = undefined + dbPromise = Promise.resolve(undefined) + } + } + }) +} + +interface TokenPayload { + workspace?: WorkspaceUuid + account?: PersonUuid + extra?: any +} + +/** + * @public + */ +function decodeTokenPayload (token: string): TokenPayload { + try { + return JSON.parse(atob(token.split('.')[1])) + } catch (err: any) { + console.error(err) + return {} + } +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export default async () => { + return { + function: { + GetClient: async (token: string, endpoint: string, opt?: ClientFactoryOptions): Promise => { + const filterModel = getMetadata(clientPlugin.metadata.FilterModel) ?? 'none' + const extraFilter = getMetadata(clientPlugin.metadata.ExtraFilter) ?? [] + + const handler = async (handler: TxHandler): Promise => { + const url = concatLink(endpoint, `/${token}`) + + const upgradeHandler: TxHandler = (...txes: Tx[]) => { + for (const tx of txes) { + if (tx?._class === core.class.TxModelUpgrade) { + opt?.onUpgrade?.() + return + } + if (tx?._class === core.class.TxWorkspaceEvent) { + const event = tx as TxWorkspaceEvent + if (event.event === WorkspaceEvent.MaintenanceNotification) { + void setPlatformStatus( + new Status(Severity.WARNING, platform.status.MaintenanceWarning, { + time: event.params.timeMinutes, + message: event.params.message ?? '' + }) + ) + } + } + } + handler(...txes) + } + const tokenPayload = decodeTokenPayload(token) + if (tokenPayload.workspace === undefined || tokenPayload.account === undefined) { + throw new Error('Workspace or account not found in token') + } + + const newOpt = { ...opt } + const connectTimeout = opt?.connectionTimeout ?? getMetadata(clientPlugin.metadata.ConnectionTimeout) + let connectPromise: Promise | undefined + if ((connectTimeout ?? 0) > 0) { + connectPromise = new Promise((resolve, reject) => { + const connectTO = setTimeout(() => { + if (!clientConnection.isConnected()) { + newOpt.onConnect = undefined + void clientConnection?.close() + void opt?.onDialTimeout?.() + reject(new Error(`Connection timeout, and no connection established to ${endpoint}`)) + } + }, connectTimeout) + newOpt.onConnect = async (event, lastTx, data) => { + try { + await opt?.onConnect?.(event, lastTx, data) + } catch (error: any) { + void clientConnection?.close() + void opt?.onDialTimeout?.() + reject(error) + return + } + + if (event !== ClientConnectEvent.Maintenance) { + // Any event is fine, it means server is alive. + clearTimeout(connectTO) + resolve() + } + } + }) + } + const clientConnection = connect(url, upgradeHandler, tokenPayload.workspace, tokenPayload.account, newOpt) + if (connectPromise !== undefined) { + await connectPromise + } + return await Promise.resolve(clientConnection) + } + + const modelFilter: ModelFilter = (txes) => { + if (filterModel === 'client') { + return returnClientTxes(txes) + } + if (filterModel === 'ui') { + return returnUITxes(txes, extraFilter) + } + return txes + } + + const client = createClient(handler, modelFilter, createModelPersistence(getWSFromToken(token)), opt?.ctx) + return await client + } + } + } +} +function returnUITxes (txes: Tx[], extraFilter: string[]): Tx[] { + const configs = new Map, PluginConfiguration>() + fillConfiguration(txes, configs) + + const allowedPlugins = [...getPlugins(), ...(getMetadata(clientPlugin.metadata.ExtraPlugins) ?? [])] + const excludedPlugins = Array.from(configs.values()).filter( + (it) => !it.enabled || !allowedPlugins.includes(it.pluginId) || extraFilter.includes(it.pluginId) + ) + return pluginFilterTx(excludedPlugins, configs, txes) +} + +function returnClientTxes (txes: Tx[]): Tx[] { + const configs = new Map, PluginConfiguration>() + fillConfiguration(txes, configs) + const excludedPlugins = Array.from(configs.values()).filter((it) => !it.enabled || it.pluginId.startsWith('server-')) + + const toExclude = new Set([ + 'workbench:class:Application' as Ref>, + 'presentation:class:ComponentPointExtension' as Ref>, + 'presentation:class:ObjectSearchCategory' as Ref>, + 'notification:class:NotificationGroup' as Ref>, + 'notification:class:NotificationType' as Ref>, + 'view:class:Action' as Ref>, + 'view:class:Viewlet' as Ref>, + 'text-editor:class:TextEditorAction' as Ref>, + 'templates:class:TemplateField' as Ref>, + 'activity:class:DocUpdateMessageViewlet' as Ref>, + 'core:class:DomainIndexConfiguration' as Ref>, + 'view:class:ViewletDescriptor' as Ref>, + 'presentation:class:ComponentPointExtension' as Ref>, + 'activity:class:ActivityMessagesFilter' as Ref>, + 'view:class:ActionCategory' as Ref>, + 'activity:class:ActivityExtension' as Ref>, + 'chunter:class:ChatMessageViewlet' as Ref>, + 'activity:class:ActivityMessageControl' as Ref>, + 'notification:class:ActivityNotificationViewlet' as Ref>, + 'setting:class:SettingsCategory' as Ref>, + 'setting:class:WorkspaceSettingCategory' as Ref>, + 'notification:class:NotificationProvider' as Ref> + ]) + + const result = pluginFilterTx(excludedPlugins, configs, txes).filter((tx) => { + // Exclude all matched UI plugins + if ( + tx?._class === core.class.TxCreateDoc || + tx?._class === core.class.TxUpdateDoc || + tx?._class === core.class.TxRemoveDoc + ) { + const cud = tx as TxCUD + if (toExclude.has(cud.objectClass)) { + return false + } + } + return true + }) + return result +} + +function createModelPersistence (workspace: string): TxPersistenceStore | undefined { + const overrideStore = getMetadata(clientPlugin.metadata.OverridePersistenceStore) + if (overrideStore !== undefined) { + return overrideStore + } + + return { + load: async () => { + const db = await dbPromise + if (db !== undefined) { + try { + const transaction = db.transaction('model', 'readwrite') // (1) + const models = transaction.objectStore('model') // (2) + const model = await new Promise<{ id: string, model: LoadModelResponse } | undefined>((resolve) => { + const storedValue: IDBRequest<{ id: string, model: LoadModelResponse }> = models.get(workspace) + storedValue.onsuccess = function () { + resolve(storedValue.result) + } + storedValue.onerror = function () { + resolve(undefined) + } + }) + + if (model == null) { + return { + full: false, + transactions: [], + hash: '' + } + } + return model.model + } catch (err: any) { + // Assume no model is stored. + } + } + return { + full: true, + transactions: [], + hash: '' + } + }, + store: async (model) => { + const db = await dbPromise + if (db !== undefined) { + const transaction = db.transaction('model', 'readwrite') // (1) + const models = transaction.objectStore('model') // (2) + models.put({ id: workspace, model }) + } + } + } +} + +function getWSFromToken (token: string): string { + const parts = token.split('.') + + const payload = parts[1] + + const decodedPayload = atob(payload) + + const parsedPayload = JSON.parse(decodedPayload) + + return parsedPayload.workspace +} diff --git a/foundations/core/packages/client-resources/tsconfig.json b/foundations/core/packages/client-resources/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/client-resources/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/client/.eslintrc.js b/foundations/core/packages/client/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/client/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/client/.npmignore b/foundations/core/packages/client/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/client/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/client/CHANGELOG.json b/foundations/core/packages/client/CHANGELOG.json new file mode 100644 index 0000000000..ff86834d5d --- /dev/null +++ b/foundations/core/packages/client/CHANGELOG.json @@ -0,0 +1,86 @@ +{ + "name": "@hcengineering/client", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/client_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.6", + "tag": "@hcengineering/client_v0.7.6", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/client_v0.7.5", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/client_v0.7.4", + "date": "Thu, 09 Oct 2025 16:57:55 GMT", + "comments": { + "patch": [ + { + "comment": "fix formatting" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/client_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/client/CHANGELOG.md b/foundations/core/packages/client/CHANGELOG.md new file mode 100644 index 0000000000..00f4521d7d --- /dev/null +++ b/foundations/core/packages/client/CHANGELOG.md @@ -0,0 +1,35 @@ +# Change Log - @hcengineering/client + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.6 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.5 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.4 +Thu, 09 Oct 2025 16:57:55 GMT + +### Patches + +- fix formatting + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/client/config/rig.json b/foundations/core/packages/client/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/client/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/client/jest.config.js b/foundations/core/packages/client/jest.config.js new file mode 100644 index 0000000000..ba719f1162 --- /dev/null +++ b/foundations/core/packages/client/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'], + testTimeout: 10000 // 10 seconds timeout for all tests +} diff --git a/foundations/core/packages/client/package.json b/foundations/core/packages/client/package.json new file mode 100644 index 0000000000..b1e3b31a95 --- /dev/null +++ b/foundations/core/packages/client/package.json @@ -0,0 +1,58 @@ +{ + "name": "@hcengineering/client", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent --coverage", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/node": "^22.18.1", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/platform": "workspace:^0.7.18", + "@hcengineering/core": "workspace:^0.7.22" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/client/src/__tests__/client.test.ts b/foundations/core/packages/client/src/__tests__/client.test.ts new file mode 100644 index 0000000000..8e71ac8fb6 --- /dev/null +++ b/foundations/core/packages/client/src/__tests__/client.test.ts @@ -0,0 +1,478 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { + Class, + ClientConnectEvent, + ClientConnection, + createClient, + Doc, + DocChunk, + DocumentQuery, + Domain, + DOMAIN_MODEL, + DOMAIN_TX, + FindOptions, + FindResult, + generateId, + Hierarchy, + LoadModelResponse, + ModelDb, + Ref, + SearchOptions, + SearchQuery, + SearchResult, + Timestamp, + Tx, + TxCreateDoc, + TxDb, + TxHandler, + TxResult, + type DomainParams, + type DomainRequestOptions, + type DomainResult, + type OperationDomain, + WorkspaceEvent, + TxWorkspaceEvent, + Client, + ClassifierKind, + TxFactory, + type Data, + type Obj +} from '@hcengineering/core' +import type { IntlString } from '@hcengineering/platform' + +const txFactory = new TxFactory(core.account.System) + +function createClass (_class: Ref>, attributes: Data>): TxCreateDoc { + return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class) +} + +// Minimal model for testing - similar to query tests +function generateMinimalModel (): Tx[] { + const txes: Tx[] = [] + + // Fill Tx'es with basic model classes + txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS })) + txes.push( + createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS }) + ) + txes.push( + createClass(core.class.Class, { + label: 'Class' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + txes.push( + createClass(core.class.Space, { + label: 'Space' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + txes.push( + createClass(core.class.Tx, { + label: 'Tx' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TX + }) + ) + txes.push( + createClass(core.class.TxCUD, { + label: 'TxCUD' as IntlString, + extends: core.class.Tx, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TX + }) + ) + txes.push( + createClass(core.class.TxCreateDoc, { + label: 'TxCreateDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxUpdateDoc, { + label: 'TxUpdateDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxRemoveDoc, { + label: 'TxRemoveDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + + return txes +} + +class TestConnection implements ClientConnection { + private readonly hierarchy: Hierarchy + private readonly model: ModelDb + private readonly transactions: TxDb + private readonly handlers: TxHandler[] = [] + private _connected: boolean = true + onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise + getLastHash?: () => Promise + + constructor (txes: Tx[]) { + this.hierarchy = new Hierarchy() + for (const tx of txes) { + this.hierarchy.tx(tx) + } + + this.transactions = new TxDb(this.hierarchy) + this.model = new ModelDb(this.hierarchy) + for (const tx of txes) { + void this.transactions.tx(tx) + void this.model.tx(tx) + } + } + + isConnected (): boolean { + return this._connected + } + + setConnected (value: boolean): void { + this._connected = value + } + + pushHandler (handler: TxHandler): void { + this.handlers.push(handler) + } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + const domain = this.hierarchy.getClass(_class).domain + if (domain === DOMAIN_TX) { + return await this.transactions.findAll(_class, query, options) + } + return await this.model.findAll(_class, query, options) + } + + async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { + return { docs: [] } + } + + async tx (tx: Tx): Promise { + if (tx.objectSpace === core.space.Model) { + this.hierarchy.tx(tx) + await this.model.tx(tx) + } + await this.transactions.tx(tx) + + // Notify handlers + this.handlers.forEach((h) => { + h(tx) + }) + + return {} + } + + async loadModel (last: Timestamp, hash?: string): Promise { + const txes = await this.transactions.findAll(core.class.Tx, { + objectSpace: core.space.Model, + modifiedOn: { $gt: last } + }) + + if (hash !== undefined) { + return { + transactions: txes, + hash: 'test-hash', + full: false + } + } + + return txes + } + + async close (): Promise { + this._connected = false + } + + async loadChunk (domain: Domain, idx?: number): Promise { + return { + idx: idx ?? 0, + docs: [], + finished: true + } + } + + async getDomainHash (domain: Domain): Promise { + return 'test-hash' + } + + async closeChunk (idx: number): Promise {} + + async loadDocs (domain: Domain, docs: Ref[]): Promise { + return [] + } + + async upload (domain: Domain, docs: Doc[]): Promise {} + + async clean (domain: Domain, docs: Ref[]): Promise {} + + async sendForceClose (): Promise {} + + async domainRequest ( + ctx: OperationDomain, + params: DomainParams, + options?: DomainRequestOptions + ): Promise { + return { domain: ctx, value: null } + } + + // Simulate receiving transactions from server + simulateTransaction (tx: Tx): void { + this.handlers.forEach((h) => { + h(tx) + }) + } +} + +describe('Client Core Implementation', () => { + let testConnection: TestConnection + let client: Client + + beforeEach(async () => { + const txes = generateMinimalModel() + testConnection = new TestConnection(txes) + + client = await createClient(async (handler: TxHandler) => { + testConnection.pushHandler(handler) + return testConnection + }) + }) + + it('should create a client instance', () => { + expect(client).toBeDefined() + expect(client.getHierarchy()).toBeDefined() + expect(client.getModel()).toBeDefined() + }) + + it('should handle findAll operations', async () => { + const spaces = await client.findAll(core.class.Space, {}) + expect(spaces).toBeDefined() + expect(Array.isArray(spaces)).toBe(true) + }) + + it('should handle findOne operations', async () => { + const space = await client.findOne(core.class.Space, { name: 'TestSpace' }) + if (space !== undefined) { + expect(space.name).toBe('TestSpace') + } + }) + + it('should handle tx operations', async () => { + const tx: TxCreateDoc = { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + objectId: generateId(), + objectClass: core.class.Space, + objectSpace: core.space.Model, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + createdOn: Date.now(), + attributes: { + name: 'NewSpace', + description: '', + private: false, + archived: false, + members: [] + } + } + + const result = await client.tx(tx) + expect(result).toBeDefined() + }) + + it('should handle updateFromRemote for model transactions', async () => { + const notifySpy = jest.fn() + client.notify = notifySpy + + const tx: TxCreateDoc = { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + objectId: generateId(), + objectClass: core.class.Space, + objectSpace: core.space.Model, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + createdOn: Date.now(), + attributes: { + name: 'RemoteSpace', + description: '', + private: false, + archived: false, + members: [] + } + } + + testConnection.simulateTransaction(tx) + + // Wait for async operations + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(notifySpy).toHaveBeenCalled() + }) + + it('should close connection properly', async () => { + await client.close() + expect(testConnection.isConnected()).toBe(false) + }) + + it('should handle domainRequest', async () => { + const result = await client.domainRequest('test-domain' as OperationDomain, {}) + expect(result).toBeDefined() + }) + + it('should update hierarchy and model on model transactions', async () => { + const hierarchy = client.getHierarchy() + const model = client.getModel() + + expect(hierarchy).toBeDefined() + expect(model).toBeDefined() + + const tx: TxCreateDoc = { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + objectId: generateId(), + objectClass: core.class.Space, + objectSpace: core.space.Model, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + createdOn: Date.now(), + attributes: { + name: 'ModelSpace', + description: '', + private: false, + archived: false, + members: [] + } + } + + await client.tx(tx) + + // Verify the transaction was applied + const spaces = await client.findAll(core.class.Space, { name: 'ModelSpace' }) + expect(spaces.length).toBeGreaterThanOrEqual(0) + }) + + it('should handle reconnection events', async () => { + let eventReceived: ClientConnectEvent | undefined + + const onConnectHandler = async ( + event: ClientConnectEvent, + lastTx: string | undefined, + data: any + ): Promise => { + eventReceived = event + } + + testConnection.onConnect = onConnectHandler + + if (testConnection.onConnect !== undefined) { + await testConnection.onConnect(ClientConnectEvent.Connected, undefined, {}) + } + + expect(eventReceived).toBe(ClientConnectEvent.Connected) + }) + + it('should handle workspace events', async () => { + const notifySpy = jest.fn() + client.notify = notifySpy + + const workspaceTx: TxWorkspaceEvent = { + _id: generateId(), + _class: core.class.TxWorkspaceEvent, + space: core.space.Tx, + objectSpace: core.space.Workspace, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + createdOn: Date.now(), + event: WorkspaceEvent.LastTx, + params: { lastTx: 'test-last-tx' } + } + + testConnection.simulateTransaction(workspaceTx) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(notifySpy).toHaveBeenCalled() + }) + + it('should handle searchFulltext', async () => { + const query: SearchQuery = { query: 'test' } + const options: SearchOptions = {} + const result = await client.searchFulltext(query, options) + expect(result).toBeDefined() + expect(result.docs).toBeDefined() + }) + + it('should handle mixin updates in findAll', async () => { + const spaces = await client.findAll(core.class.Space, {}, { limit: 10 }) + expect(spaces).toBeDefined() + expect(spaces.total).toBeDefined() + }) + + it('should skip already applied model transactions', async () => { + const tx: TxCreateDoc = { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + objectId: generateId(), + objectClass: core.class.Space, + objectSpace: core.space.Model, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + createdOn: Date.now(), + attributes: { + name: 'DuplicateSpace', + description: '', + private: false, + archived: false, + members: [] + } + } + + // Apply the same transaction twice + await client.tx(tx) + + const notifySpy = jest.fn() + client.notify = notifySpy + + // Simulate receiving the same transaction from remote + testConnection.simulateTransaction(tx) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Should still notify but skip model update + expect(notifySpy).toHaveBeenCalled() + }) +}) diff --git a/foundations/core/packages/client/src/index.ts b/foundations/core/packages/client/src/index.ts new file mode 100644 index 0000000000..ae6f530020 --- /dev/null +++ b/foundations/core/packages/client/src/index.ts @@ -0,0 +1,101 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Client, ClientConnectEvent, MeasureContext, TxPersistenceStore } from '@hcengineering/core' +import { type Plugin, type Resource, type Metadata, plugin } from '@hcengineering/platform' + +/** + * @public + */ +export const clientId = 'client' as Plugin + +/** + * @public + */ +export type ClientSocketFactory = (url: string) => ClientSocket + +/** + * @public + */ +export interface ClientSocket { + onmessage?: ((this: ClientSocket, ev: MessageEvent) => any) | null + onclose?: ((this: ClientSocket, ev: CloseEvent) => any) | null + onopen?: ((this: ClientSocket, ev: Event) => any) | null + onerror?: ((this: ClientSocket, ev: Event) => any) | null + + send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void + + close: (code?: number) => void + + readyState: ClientSocketReadyState + + bufferedAmount?: number +} + +/** + * @public + */ +export enum ClientSocketReadyState { + CONNECTING = 0, + OPEN = 1, + CLOSING = 2, + CLOSED = 3 +} + +export interface ClientFactoryOptions { + socketFactory?: ClientSocketFactory + useBinaryProtocol?: boolean + useProtocolCompression?: boolean + connectionTimeout?: number + onHello?: (serverVersion?: string) => boolean + onUpgrade?: () => void + onUnauthorized?: () => void + onArchived?: () => void + onMigration?: () => void + onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise + ctx?: MeasureContext + onDialTimeout?: () => void | Promise + + useGlobalRPCHandler?: boolean +} + +/** + * @public + */ +export type ClientFactory = (token: string, endpoint: string, opt?: ClientFactoryOptions) => Promise + +// client - will filter out all server model elements +// It will also filter out all UI Elements, like Actions, View declarations etc. +// ui - will filter out all server element's and all UI disabled elements. +export type FilterMode = 'none' | 'client' | 'ui' + +export const pingConst = 'ping' +export const pongConst = 'pong!' + +export default plugin(clientId, { + metadata: { + ClientSocketFactory: '' as Metadata, + FilterModel: '' as Metadata, + ExtraFilter: '' as Metadata, + ExtraPlugins: '' as Metadata, + UseBinaryProtocol: '' as Metadata, + UseProtocolCompression: '' as Metadata, + ConnectionTimeout: '' as Metadata, + OverridePersistenceStore: '' as Metadata + }, + function: { + GetClient: '' as Resource + } +}) diff --git a/foundations/core/packages/client/tsconfig.json b/foundations/core/packages/client/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/client/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/collaborator-client/.eslintrc.js b/foundations/core/packages/collaborator-client/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/collaborator-client/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/collaborator-client/.npmignore b/foundations/core/packages/collaborator-client/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/collaborator-client/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/collaborator-client/CHANGELOG.json b/foundations/core/packages/collaborator-client/CHANGELOG.json new file mode 100644 index 0000000000..22653fa81c --- /dev/null +++ b/foundations/core/packages/collaborator-client/CHANGELOG.json @@ -0,0 +1,63 @@ +{ + "name": "@hcengineering/collaborator-client", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/collaborator-client_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/collaborator-client_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/collaborator-client_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/collaborator-client_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/collaborator-client/CHANGELOG.md b/foundations/core/packages/collaborator-client/CHANGELOG.md new file mode 100644 index 0000000000..ab60b3d896 --- /dev/null +++ b/foundations/core/packages/collaborator-client/CHANGELOG.md @@ -0,0 +1,28 @@ +# Change Log - @hcengineering/collaborator-client + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/collaborator-client/config/rig.json b/foundations/core/packages/collaborator-client/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/collaborator-client/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/collaborator-client/jest.config.js b/foundations/core/packages/collaborator-client/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/collaborator-client/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/collaborator-client/package.json b/foundations/core/packages/collaborator-client/package.json new file mode 100644 index 0000000000..483f97c769 --- /dev/null +++ b/foundations/core/packages/collaborator-client/package.json @@ -0,0 +1,59 @@ +{ + "name": "@hcengineering/collaborator-client", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent --coverage", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "cross-env": "~7.0.3", + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@types/node": "^22.18.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "esbuild": "^0.25.10", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/collaborator-client/src/__tests__/utils.test.ts b/foundations/core/packages/collaborator-client/src/__tests__/utils.test.ts new file mode 100644 index 0000000000..06ca9d9dde --- /dev/null +++ b/foundations/core/packages/collaborator-client/src/__tests__/utils.test.ts @@ -0,0 +1,39 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { CollaborativeDoc, Doc, Ref } from '@hcengineering/core' +import { encodeDocumentId, decodeDocumentId } from '../utils' + +describe('utils', () => { + it('encodeDocumentId', () => { + const doc: CollaborativeDoc = { + objectClass: core.class.Doc, + objectId: 'doc1' as Ref, + objectAttr: 'description' + } + expect(encodeDocumentId('ws1', doc)).toEqual('ws1|core:class:Doc|doc1|description') + }) + + describe('decodeDocumentId', () => { + expect(decodeDocumentId('ws1|core:class:Doc|doc1|description')).toEqual({ + workspaceId: 'ws1', + documentId: { + objectClass: core.class.Doc, + objectId: 'doc1' as Ref, + objectAttr: 'description' + } + }) + }) +}) diff --git a/foundations/core/packages/collaborator-client/src/client.ts b/foundations/core/packages/collaborator-client/src/client.ts new file mode 100644 index 0000000000..8cabaf1285 --- /dev/null +++ b/foundations/core/packages/collaborator-client/src/client.ts @@ -0,0 +1,163 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Blob, CollaborativeDoc, Markup, MarkupBlobRef, Ref, WorkspaceUuid, concatLink } from '@hcengineering/core' +import { encodeDocumentId } from './utils' + +/** @public */ +export interface GetContentRequest { + source?: Ref +} + +/** @public */ +export interface GetContentResponse { + content: Record +} + +/** @public */ +export interface CreateContentRequest { + content: Record +} + +/** @public */ +export interface CreateContentResponse { + content: Record +} + +/** @public */ +export interface UpdateContentRequest { + content: Record +} + +/** @public */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UpdateContentResponse {} + +/** @public */ +export interface CollaboratorClient { + getMarkup: (document: CollaborativeDoc, source?: Ref | null) => Promise + createMarkup: (document: CollaborativeDoc, markup: Markup) => Promise + updateMarkup: (document: CollaborativeDoc, markup: Markup) => Promise + copyContent: (source: CollaborativeDoc, target: CollaborativeDoc) => Promise +} + +/** @public */ +export function getClient (workspaceId: WorkspaceUuid, token: string, collaboratorUrl: string): CollaboratorClient { + const url = collaboratorUrl.replaceAll('wss://', 'https://').replace('ws://', 'http://') + return new CollaboratorClientImpl(workspaceId, token, url) +} + +class CollaboratorClientImpl implements CollaboratorClient { + constructor ( + private readonly workspace: WorkspaceUuid, + private readonly token: string, + private readonly collaboratorUrl: string + ) {} + + private async rpc(document: CollaborativeDoc, method: string, payload: P): Promise { + const workspace = this.workspace + const documentId = encodeDocumentId(workspace, document) + + const url = concatLink(this.collaboratorUrl, `/rpc/${encodeURIComponent(documentId)}`) + + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + this.token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ method, payload }) + }) + + if (!res.ok) { + throw new Error('HTTP error ' + res.status) + } + + const result = await res.json() + + if (result.error != null) { + throw new Error(result.error) + } + + return result as R + } + + async getMarkup (document: CollaborativeDoc, source?: Ref | null): Promise { + const payload: GetContentRequest = { + source: source !== null ? source : undefined + } + + const res = await retry( + 3, + async () => { + return await this.rpc(document, 'getContent', payload) + }, + 50 + ) + + return res.content[document.objectAttr] ?? '' + } + + async createMarkup (document: CollaborativeDoc, markup: Markup): Promise { + const content = { + [document.objectAttr]: markup + } + + const res = await retry( + 3, + async () => { + return await this.rpc(document, 'createContent', { content }) + }, + 50 + ) + + return res.content[document.objectAttr] + } + + async updateMarkup (document: CollaborativeDoc, markup: Markup): Promise { + const content = { + [document.objectAttr]: markup + } + + await retry( + 3, + async () => { + await this.rpc(document, 'updateContent', { content }) + }, + 50 + ) + } + + async copyContent (source: CollaborativeDoc, target: CollaborativeDoc, content?: Ref): Promise { + const markup = await this.getMarkup(source, content) + await this.updateMarkup(target, markup) + } +} + +async function retry (retries: number, op: () => Promise, delay: number = 100): Promise { + let error: any + while (retries > 0) { + retries-- + try { + return await op() + } catch (err: any) { + error = err + if (retries !== 0) { + await new Promise((resolve) => setTimeout(resolve, delay)) + } + } + } + throw error +} diff --git a/foundations/core/packages/collaborator-client/src/index.ts b/foundations/core/packages/collaborator-client/src/index.ts new file mode 100644 index 0000000000..70baf83124 --- /dev/null +++ b/foundations/core/packages/collaborator-client/src/index.ts @@ -0,0 +1,17 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './client' +export * from './utils' diff --git a/foundations/core/packages/collaborator-client/src/utils.ts b/foundations/core/packages/collaborator-client/src/utils.ts new file mode 100644 index 0000000000..f6225ba0aa --- /dev/null +++ b/foundations/core/packages/collaborator-client/src/utils.ts @@ -0,0 +1,38 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Class, CollaborativeDoc, Doc, Ref } from '@hcengineering/core' + +/** @public */ +export function encodeDocumentId (workspaceId: string, documentId: CollaborativeDoc): string { + const { objectClass, objectId, objectAttr } = documentId + return [workspaceId, objectClass, objectId, objectAttr].join('|') +} + +/** @public */ +export function decodeDocumentId (documentId: string): { + workspaceId: string + documentId: CollaborativeDoc +} { + const [workspaceId, objectClass, objectId, objectAttr] = documentId.split('|') + return { + workspaceId, + documentId: { + objectClass: objectClass as Ref>, + objectId: objectId as Ref, + objectAttr + } + } +} diff --git a/foundations/core/packages/collaborator-client/tsconfig.json b/foundations/core/packages/collaborator-client/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/collaborator-client/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/core/.eslintrc.js b/foundations/core/packages/core/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/core/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/core/.npmignore b/foundations/core/packages/core/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/core/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/core/CHANGELOG.json b/foundations/core/packages/core/CHANGELOG.json new file mode 100644 index 0000000000..18df5a14de --- /dev/null +++ b/foundations/core/packages/core/CHANGELOG.json @@ -0,0 +1,283 @@ +{ + "name": "@hcengineering/core", + "entries": [ + { + "version": "0.7.22", + "tag": "@hcengineering/core_v0.7.22", + "date": "Thu, 06 Nov 2025 09:09:58 GMT", + "comments": { + "patch": [ + { + "comment": "Add txMatch to permission" + } + ] + } + }, + { + "version": "0.7.21", + "tag": "@hcengineering/core_v0.7.21", + "date": "Thu, 30 Oct 2025 23:11:55 GMT", + "comments": { + "patch": [ + { + "comment": "extend TxAccessLevel" + } + ] + } + }, + { + "version": "0.7.20", + "tag": "@hcengineering/core_v0.7.20", + "date": "Thu, 30 Oct 2025 08:41:42 GMT", + "comments": { + "patch": [ + { + "comment": "add workspace usae info" + } + ] + } + }, + { + "version": "0.7.19", + "tag": "@hcengineering/core_v0.7.19", + "date": "Wed, 29 Oct 2025 07:46:42 GMT", + "comments": { + "patch": [ + { + "comment": "Suspend exceptions from connect in traces" + } + ] + } + }, + { + "version": "0.7.18", + "tag": "@hcengineering/core_v0.7.18", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "patch": [ + { + "comment": "Rank for attributes" + } + ] + } + }, + { + "version": "0.7.10", + "tag": "@hcengineering/core_v0.7.10", + "date": "Tue, 21 Oct 2025 19:04:55 GMT", + "comments": { + "patch": [ + { + "comment": "Add TypeIdentifier for custom attribute incremental IDs" + } + ] + } + }, + { + "version": "0.7.8", + "tag": "@hcengineering/core_v0.7.8", + "date": "Wed, 15 Oct 2025 18:01:30 GMT", + "comments": { + "patch": [ + { + "comment": "adjusted interfaces" + } + ] + } + }, + { + "version": "0.7.7", + "tag": "@hcengineering/core_v0.7.7", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.6", + "tag": "@hcengineering/core_v0.7.6", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/core_v0.7.5", + "date": "Thu, 09 Oct 2025 16:57:55 GMT", + "comments": { + "patch": [ + { + "comment": "Hierarchy improvements" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/core_v0.7.4", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "patch": [ + { + "comment": "Fix issue with time rate limiter" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/core_v0.6.11", + "date": "Fri, 20 Aug 2021 16:21:03 GMT", + "comments": { + "patch": [ + { + "comment": "Transaction ordering" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/core_v0.6.10", + "date": "Wed, 11 Aug 2021 09:37:04 GMT", + "comments": { + "patch": [ + { + "comment": "Server support for workspaces" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/core_v0.6.8", + "date": "Sun, 08 Aug 2021 21:05:26 GMT", + "comments": { + "patch": [ + { + "comment": "Fix server connection" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/core_v0.6.7", + "date": "Wed, 04 Aug 2021 21:18:44 GMT", + "comments": { + "patch": [ + { + "comment": "fix" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.2` to `~0.6.3`" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/core_v0.6.6", + "date": "Wed, 04 Aug 2021 21:00:14 GMT", + "comments": { + "patch": [ + { + "comment": "npmigonre" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/core_v0.6.5", + "date": "Wed, 04 Aug 2021 20:26:15 GMT", + "comments": { + "patch": [ + { + "comment": "Add missed docs" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/core_v0.6.4", + "date": "Wed, 04 Aug 2021 18:12:38 GMT", + "comments": { + "patch": [ + { + "comment": "Add `memdb` docs" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/core_v0.6.3", + "date": "Wed, 04 Aug 2021 18:05:12 GMT", + "comments": { + "patch": [ + { + "comment": "Add `hierarchy.ts` docs" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/core_v0.6.2", + "date": "Wed, 04 Aug 2021 17:53:24 GMT", + "comments": { + "patch": [ + { + "comment": "Add documentation" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/core_v0.6.1", + "date": "Wed, 04 Aug 2021 17:38:30 GMT", + "comments": { + "patch": [ + { + "comment": "Minor changes for publish" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.0` to `~0.6.1`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/core/CHANGELOG.md b/foundations/core/packages/core/CHANGELOG.md new file mode 100644 index 0000000000..b1b6d51db5 --- /dev/null +++ b/foundations/core/packages/core/CHANGELOG.md @@ -0,0 +1,151 @@ +# Change Log - @hcengineering/core + +This log was last generated on Thu, 06 Nov 2025 09:09:58 GMT and should not be manually modified. + +## 0.7.22 +Thu, 06 Nov 2025 09:09:58 GMT + +### Patches + +- Add txMatch to permission + +## 0.7.21 +Thu, 30 Oct 2025 23:11:55 GMT + +### Patches + +- extend TxAccessLevel + +## 0.7.20 +Thu, 30 Oct 2025 08:41:42 GMT + +### Patches + +- add workspace usae info + +## 0.7.19 +Wed, 29 Oct 2025 07:46:42 GMT + +### Patches + +- Suspend exceptions from connect in traces + +## 0.7.18 +Mon, 27 Oct 2025 13:27:12 GMT + +### Patches + +- Rank for attributes + +## 0.7.10 +Tue, 21 Oct 2025 19:04:55 GMT + +### Patches + +- Add TypeIdentifier for custom attribute incremental IDs + +## 0.7.8 +Wed, 15 Oct 2025 18:01:30 GMT + +### Patches + +- adjusted interfaces + +## 0.7.7 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.6 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.5 +Thu, 09 Oct 2025 16:57:55 GMT + +### Patches + +- Hierarchy improvements + +## 0.7.4 +Wed, 08 Oct 2025 03:40:53 GMT + +### Patches + +- Fix issue with time rate limiter + +## 0.7.0 +Fri, 20 Aug 2021 16:21:03 GMT + +### Patches + +- Transaction ordering + +## 0.7.0 +Wed, 11 Aug 2021 09:37:04 GMT + +### Patches + +- Server support for workspaces + +## 0.7.0 +Sun, 08 Aug 2021 21:05:26 GMT + +### Patches + +- Fix server connection + +## 0.7.0 +Wed, 04 Aug 2021 21:18:44 GMT + +### Patches + +- fix + +## 0.7.0 +Wed, 04 Aug 2021 21:00:14 GMT + +### Patches + +- npmigonre + +## 0.7.0 +Wed, 04 Aug 2021 20:26:15 GMT + +### Patches + +- Add missed docs + +## 0.7.0 +Wed, 04 Aug 2021 18:12:38 GMT + +### Patches + +- Add `memdb` docs + +## 0.7.0 +Wed, 04 Aug 2021 18:05:12 GMT + +### Patches + +- Add `hierarchy.ts` docs + +## 0.7.0 +Wed, 04 Aug 2021 17:53:24 GMT + +### Patches + +- Add documentation + +## 0.7.0 +Wed, 04 Aug 2021 17:38:30 GMT + +### Patches + +- Minor changes for publish + diff --git a/foundations/core/packages/core/config/rig.json b/foundations/core/packages/core/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/core/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/core/jest.config.js b/foundations/core/packages/core/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/core/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/core/lang/cs.json b/foundations/core/packages/core/lang/cs.json new file mode 100644 index 0000000000..8a312b656e --- /dev/null +++ b/foundations/core/packages/core/lang/cs.json @@ -0,0 +1,72 @@ +{ + "string": { + "Id": "Id", + "Space": "Prostor", + "Spaces": "Prostory", + "SpacesDescription": "Spravujte typ všech prostor", + "TypedSpace": "Typovaný prostor", + "SpaceType": "Typ prostoru", + "Modified": "Upraveno", + "ModifiedDate": "Datum úpravy", + "ModifiedBy": "Upravil", + "Class": "Třída", + "AttachedTo": "Připojeno k", + "AttachedToClass": "Připojeno k třídě", + "Name": "Název", + "Description": "Popis", + "ShortDescription": "Krátký popis", + "Descriptor": "Popisovač", + "TargetClass": "Cílová třída", + "Role": "Role", + "Roles": "Role", + "Private": "Soukromé", + "Archived": "Archivováno", + "ClassLabel": "Typ", + "ClassPropertyLabel": "Štítek", + "String": "Text", + "Markup": "Značení", + "Number": "Číslo", + "Boolean": "Zaškrtávací políčko", + "Timestamp": "Časové razítko", + "Date": "Datum", + "IntlString": "Mezinárodní řetězec", + "Ref": "Odkaz", + "Collection": "Kolekce", + "Array": "Vícenásobný výběr", + "Enum": "Výběr", + "Members": "Členové", + "Hyperlink": "URL", + "Collaborative": "Spolupráce", + "Object": "Objekt", + "System": "Systém", + "CreatedBy": "Vytvořil", + "CreatedDate": "Datum vytvoření", + "Status": "Stav", + "StatusCategory": "Kategorie stavu", + "Account": "Účet", + "Rank": "Hodnost", + "Owners": "Vlastníci", + "Permission": "Oprávnění", + "CreateObject": "Vytvořit objekt", + "UpdateObject": "Aktualizovat objekt", + "DeleteObject": "Smazat objekt", + "ForbidDeleteObject": "Zakázat mazání objektu", + "UpdateSpace": "Aktualizovat prostor", + "ArchiveSpace": "Archivovat prostor", + "CreateObjectDescription": "Umožňuje uživatelům vytvářet objekty v prostoru", + "UpdateObjectDescription": "Umožňuje uživatelům aktualizovat objekty v prostoru", + "DeleteObjectDescription": "Umožňuje uživatelům mazat objekty v prostoru", + "ForbidDeleteObjectDescription": "Zakazuje uživatelům mazání objektů v prostoru", + "UpdateSpaceDescription": "Umožňuje uživatelům aktualizovat prostor", + "ArchiveSpaceDescription": "Umožňuje uživatelům archivovat prostor", + "AutoJoin": "Automatické připojení", + "AutoJoinDescr": "Automaticky připojit nové zaměstnance k tomuto prostoru", + "BlobSize": "Velikost", + "BlobContentType": "Typ obsahu", + "Relation": "Vztah", + "Relations": "Vztahy", + "AddRelation": "Přidat vztah", + "PersonId": "Osoba", + "AccountId": "Účet" + } +} diff --git a/foundations/core/packages/core/lang/de.json b/foundations/core/packages/core/lang/de.json new file mode 100644 index 0000000000..bc8768fa84 --- /dev/null +++ b/foundations/core/packages/core/lang/de.json @@ -0,0 +1,72 @@ +{ + "string": { + "Id": "Id", + "Space": "Arbeitsbereich", + "Spaces": "Arbeitsbereiche", + "SpacesDescription": "Alle Arbeitsbereichstypen verwalten", + "TypedSpace": "Typisierter Arbeitsbereich", + "SpaceType": "Arbeitsbereichstyp", + "Modified": "Geändert", + "ModifiedDate": "Änderungsdatum", + "ModifiedBy": "Geändert von", + "Class": "Klasse", + "AttachedTo": "Angehängt an", + "AttachedToClass": "Angehängt an Klasse", + "Name": "Name", + "Description": "Beschreibung", + "ShortDescription": "Kurzbeschreibung", + "Descriptor": "Bezeichner", + "TargetClass": "Zielklasse", + "Role": "Rolle", + "Roles": "Rollen", + "Private": "Privat", + "Archived": "Archiviert", + "ClassLabel": "Typ", + "ClassPropertyLabel": "Bezeichnung", + "String": "Text", + "Markup": "Formatierter Text", + "Number": "Zahl", + "Boolean": "Kontrollkästchen", + "Timestamp": "Zeitstempel", + "Date": "Datum", + "IntlString": "Internationaler Text", + "Ref": "Referenz", + "Collection": "Sammlung", + "Array": "Mehrfachauswahl", + "Enum": "Auswahl", + "Members": "Mitglieder", + "Hyperlink": "URL", + "MarkupBlobRef": "Kollaborativ", + "Object": "Objekt", + "System": "System", + "CreatedBy": "Erstellt von", + "CreatedDate": "Erstellungsdatum", + "Status": "Status", + "StatusCategory": "Statuskategorie", + "Account": "Konto", + "Rank": "Rang", + "Owners": "Eigentümer", + "Permission": "Berechtigung", + "CreateObject": "Objekt erstellen", + "UpdateObject": "Objekt aktualisieren", + "DeleteObject": "Objekt löschen", + "ForbidDeleteObject": "Objekt löschen verbieten", + "UpdateSpace": "Arbeitsbereich aktualisieren", + "ArchiveSpace": "Arbeitsbereich archivieren", + "CreateObjectDescription": "Gewährt Benutzern die Möglichkeit, Objekte im Arbeitsbereich zu erstellen", + "UpdateObjectDescription": "Gewährt Benutzern die Möglichkeit, Objekte im Arbeitsbereich zu aktualisieren", + "DeleteObjectDescription": "Gewährt Benutzern die Möglichkeit, Objekte im Arbeitsbereich zu löschen", + "ForbidDeleteObjectDescription": "Verbietet Benutzern das Löschen von Objekten im Arbeitsbereich", + "UpdateSpaceDescription": "Gewährt Benutzern die Möglichkeit, den Arbeitsbereich zu aktualisieren", + "ArchiveSpaceDescription": "Gewährt Benutzern die Möglichkeit, den Arbeitsbereich zu archivieren", + "AutoJoin": "Automatisch beitreten", + "AutoJoinDescr": "Neue Mitarbeiter automatisch diesem Arbeitsbereich hinzufügen", + "BlobSize": "Größe", + "BlobContentType": "Inhaltstyp", + "Relation": "Beziehung", + "Relations": "Beziehungen", + "AddRelation": "Beziehung hinzufügen", + "PersonId": "Person", + "AccountId": "Konto" + } +} diff --git a/foundations/core/packages/core/lang/en.json b/foundations/core/packages/core/lang/en.json new file mode 100644 index 0000000000..f9687e3159 --- /dev/null +++ b/foundations/core/packages/core/lang/en.json @@ -0,0 +1,72 @@ +{ + "string": { + "Id": "Id", + "Space": "Space", + "Spaces": "Spaces", + "SpacesDescription": "Manage all spaces' space type", + "TypedSpace": "Typed space", + "SpaceType": "Space type", + "Modified": "Modified", + "ModifiedDate": "Modified date", + "ModifiedBy": "Modified by", + "Class": "Class", + "AttachedTo": "Attached to", + "AttachedToClass": "Attached to class", + "Name": "Name", + "Description": "Description", + "ShortDescription": "Short description", + "Descriptor": "Descriptor", + "TargetClass": "Target class", + "Role": "Role", + "Roles": "Roles", + "Private": "Private", + "Archived": "Archived", + "ClassLabel": "Type", + "ClassPropertyLabel": "Label", + "String": "Text", + "Markup": "Markup", + "Number": "Number", + "Boolean": "Checkbox", + "Timestamp": "Timestamp", + "Date": "Date", + "IntlString": "IntlString", + "Ref": "Reference", + "Collection": "Collection", + "Array": "Multi-Select", + "Enum": "Select", + "Members": "Members", + "Hyperlink": "URL", + "MarkupBlobRef": "Collaborative", + "Object": "Object", + "System": "System", + "CreatedBy": "Created by", + "CreatedDate": "Created date", + "Status": "Status", + "StatusCategory": "Status category", + "Account": "Account", + "Rank": "Rank", + "Owners": "Owners", + "Permission": "Permission", + "CreateObject": "Create object", + "UpdateObject": "Update object", + "DeleteObject": "Delete object", + "ForbidDeleteObject": "Forbid delete object", + "UpdateSpace": "Update space", + "ArchiveSpace": "Archive space", + "CreateObjectDescription": "Grants users ability to create objects in the space", + "UpdateObjectDescription": "Grants users ability to update objects in the space", + "DeleteObjectDescription": "Grants users ability to delete objects in the space", + "ForbidDeleteObjectDescription": "Forbid users deleting objects in the space", + "UpdateSpaceDescription": "Grants users ability to update the space", + "ArchiveSpaceDescription": "Grants users ability to archive the space", + "AutoJoin": "Auto join", + "AutoJoinDescr": "Automatically join new employees to this space", + "BlobSize": "Size", + "BlobContentType": "Content type", + "Relation": "Relation", + "Relations": "Relations", + "AddRelation": "Add relation", + "PersonId": "Person", + "AccountId": "Account" + } +} diff --git a/foundations/core/packages/core/lang/es.json b/foundations/core/packages/core/lang/es.json new file mode 100644 index 0000000000..e6917fa281 --- /dev/null +++ b/foundations/core/packages/core/lang/es.json @@ -0,0 +1,65 @@ +{ + "string": { + "Id": "Id.", + "Space": "Espacio", + "Spaces": "Espacios", + "SpacesDescription": "Gestionar el tipo de espacio de todos los espacios", + "Modified": "Modificado", + "ModifiedDate": "Fecha de modificación", + "ModifiedBy": "Modificado por", + "Class": "Clase", + "AttachedTo": "Adjunto a", + "AttachedToClass": "Clase adjunta", + "Name": "Nombre", + "Description": "Descripción", + "Private": "Privado", + "Archived": "Archivado", + "ClassLabel": "Tipo", + "ClassPropertyLabel": "Etiqueta", + "String": "Texto", + "Markup": "Marcado", + "Number": "Número", + "Boolean": "Casilla de verificación", + "Timestamp": "Marca de tiempo", + "Date": "Fecha", + "IntlString": "Cadena de texto internacionalizada", + "Ref": "Referencia", + "Collection": "Colección", + "Array": "Selección múltiple", + "Enum": "Selección", + "Members": "Miembros", + "Hyperlink": "Enlace", + "MarkupBlobRef": "Colaborativo", + "Object": "Objeto", + "System": "Sistema", + "CreatedBy": "Creado por", + "CreatedDate": "Fecha de creación", + "Status": "Estado", + "StatusCategory": "Categoría de estado", + "Account": "Cuenta", + "Rank": "Rango", + "Owners": "Propietarios", + "Permission": "Permiso", + "CreateObject": "Crear objeto", + "UpdateObject": "Actualizar objeto", + "DeleteObject": "Eliminar objeto", + "ForbidDeleteObject": "Prohibir eliminar objeto", + "UpdateSpace": "Actualizar espacio", + "ArchiveSpace": "Archivar espacio", + "CreateObjectDescription": "Concede a los usuarios la capacidad de crear objetos en el espacio", + "UpdateObjectDescription": "Concede a los usuarios la capacidad de actualizar objetos en el espacio", + "DeleteObjectDescription": "Concede a los usuarios la capacidad de eliminar objetos en el espacio", + "ForbidDeleteObjectDescription": "Prohíbe a los usuarios eliminar objetos en el espacio", + "UpdateSpaceDescription": "Concede a los usuarios la capacidad de actualizar el espacio", + "ArchiveSpaceDescription": "Concede a los usuarios la capacidad de archivar el espacio", + "AutoJoin": "Auto unirse", + "AutoJoinDescr": "Unirse automáticamente a los nuevos empleados a este espacio", + "BlobSize": "Tamaño", + "BlobContentType": "Tipo de contenido", + "Relation": "Relación", + "Relations": "Relaciones", + "AddRelation": "Añadir relación", + "PersonId": "Id. de persona", + "AccountId": "Cuenta" + } +} diff --git a/foundations/core/packages/core/lang/fr.json b/foundations/core/packages/core/lang/fr.json new file mode 100644 index 0000000000..bb4b311b71 --- /dev/null +++ b/foundations/core/packages/core/lang/fr.json @@ -0,0 +1,72 @@ +{ + "string": { + "Id": "Id", + "Space": "Espace", + "Spaces": "Espaces", + "SpacesDescription": "Gérer le type d'espace de tous les espaces", + "TypedSpace": "Espace typé", + "SpaceType": "Type d'espace", + "Modified": "Modifié", + "ModifiedDate": "Date de modification", + "ModifiedBy": "Modifié par", + "Class": "Classe", + "AttachedTo": "Attaché à", + "AttachedToClass": "Attaché à la classe", + "Name": "Nom", + "Description": "Description", + "ShortDescription": "Description courte", + "Descriptor": "Descripteur", + "TargetClass": "Classe cible", + "Role": "Rôle", + "Roles": "Rôles", + "Private": "Privé", + "Archived": "Archivé", + "ClassLabel": "Type", + "ClassPropertyLabel": "Label", + "String": "Texte", + "Markup": "Balise", + "Number": "Nombre", + "Boolean": "Case à cocher", + "Timestamp": "Horodatage", + "Date": "Date", + "IntlString": "Chaîne internationale", + "Ref": "Référence", + "Collection": "Collection", + "Array": "Sélection multiple", + "Enum": "Sélection", + "Members": "Membres", + "Hyperlink": "URL", + "MarkupBlobRef": "Collaboratif", + "Object": "Objet", + "System": "Système", + "CreatedBy": "Créé par", + "CreatedDate": "Date de création", + "Status": "Statut", + "StatusCategory": "Catégorie de statut", + "Account": "Compte", + "Rank": "Rang", + "Owners": "Propriétaires", + "Permission": "Permission", + "CreateObject": "Créer un objet", + "UpdateObject": "Mettre à jour l'objet", + "DeleteObject": "Supprimer l'objet", + "ForbidDeleteObject": "Interdire la suppression de l'objet", + "UpdateSpace": "Mettre à jour l'espace", + "ArchiveSpace": "Archiver l'espace", + "CreateObjectDescription": "Accorde aux utilisateurs la capacité de créer des objets dans l'espace", + "UpdateObjectDescription": "Accorde aux utilisateurs la capacité de mettre à jour les objets dans l'espace", + "DeleteObjectDescription": "Accorde aux utilisateurs la capacité de supprimer des objets dans l'espace", + "ForbidDeleteObjectDescription": "Interdire aux utilisateurs de supprimer des objets dans l'espace", + "UpdateSpaceDescription": "Accorde aux utilisateurs la capacité de mettre à jour l'espace", + "ArchiveSpaceDescription": "Accorde aux utilisateurs la capacité d'archiver l'espace", + "AutoJoin": "Rejoindre automatiquement", + "AutoJoinDescr": "Ajouter automatiquement les nouveaux employés à cet espace", + "BlobSize": "Taille", + "BlobContentType": "Type de contenu", + "Relation": "Relation", + "Relations": "Relations", + "AddRelation": "Ajouter une relation", + "PersonId": "Id de personne", + "AccountId": "Compte" + } +} diff --git a/foundations/core/packages/core/lang/it.json b/foundations/core/packages/core/lang/it.json new file mode 100644 index 0000000000..68f072205e --- /dev/null +++ b/foundations/core/packages/core/lang/it.json @@ -0,0 +1,72 @@ +{ + "string": { + "Id": "Id", + "Space": "Spazio", + "Spaces": "Spazi", + "SpacesDescription": "Gestisci il tipo di spazio di tutti gli spazi", + "TypedSpace": "Spazio tipizzato", + "SpaceType": "Tipo di spazio", + "Modified": "Modificato", + "ModifiedDate": "Data di modifica", + "ModifiedBy": "Modificato da", + "Class": "Classe", + "AttachedTo": "Associato a", + "AttachedToClass": "Associato alla classe", + "Name": "Nome", + "Description": "Descrizione", + "ShortDescription": "Descrizione breve", + "Descriptor": "Descrittore", + "TargetClass": "Classe target", + "Role": "Ruolo", + "Roles": "Ruoli", + "Private": "Privato", + "Archived": "Archiviato", + "ClassLabel": "Tipo", + "ClassPropertyLabel": "Etichetta", + "String": "Testo", + "Markup": "Markup", + "Number": "Numero", + "Boolean": "Casella di controllo", + "Timestamp": "Timestamp", + "Date": "Data", + "IntlString": "StringaIntl", + "Ref": "Riferimento", + "Collection": "Collezione", + "Array": "Selezione multipla", + "Enum": "Selezione", + "Members": "Membri", + "Hyperlink": "Hyperlink", + "Collaborative": "Collaborativo", + "Object": "Oggetto", + "System": "Sistema", + "CreatedBy": "Creato da", + "CreatedDate": "Data di creazione", + "Status": "Stato", + "StatusCategory": "Categoria di stato", + "Account": "Account", + "Rank": "Livello", + "Owners": "Proprietari", + "Permission": "Permesso", + "CreateObject": "Crea oggetto", + "UpdateObject": "Aggiorna oggetto", + "DeleteObject": "Elimina oggetto", + "ForbidDeleteObject": "Proibisci eliminazione oggetto", + "UpdateSpace": "Aggiorna spazio", + "ArchiveSpace": "Archivia spazio", + "CreateObjectDescription": "Concede agli utenti la possibilità di creare oggetti nello spazio", + "UpdateObjectDescription": "Concede agli utenti la possibilità di aggiornare oggetti nello spazio", + "DeleteObjectDescription": "Concede agli utenti la possibilità di eliminare oggetti nello spazio", + "ForbidDeleteObjectDescription": "Proibisce agli utenti di eliminare oggetti nello spazio", + "UpdateSpaceDescription": "Concede agli utenti la possibilità di aggiornare lo spazio", + "ArchiveSpaceDescription": "Concede agli utenti la possibilità di archiviare lo spazio", + "AutoJoin": "Partecipazione automatica", + "AutoJoinDescr": "Aggiungi automaticamente i nuovi dipendenti a questo spazio", + "BlobSize": "Dimensione", + "BlobContentType": "Tipo di contenuto", + "Relation": "Relazione", + "Relations": "Relazioni", + "AddRelation": "Aggiungi relazione", + "PersonId": "ID persona", + "AccountId": "ID account" + } +} diff --git a/foundations/core/packages/core/lang/ja.json b/foundations/core/packages/core/lang/ja.json new file mode 100644 index 0000000000..cdb6c0daa1 --- /dev/null +++ b/foundations/core/packages/core/lang/ja.json @@ -0,0 +1,72 @@ +{ + "string": { + "Id": "ID", + "Space": "スペース", + "Spaces": "スペース一覧", + "SpacesDescription": "すべてのスペースの種類を管理します", + "TypedSpace": "種類付きスペース", + "SpaceType": "スペースの種類", + "Modified": "更新", + "ModifiedDate": "更新日", + "ModifiedBy": "更新者", + "Class": "クラス", + "AttachedTo": "接続先", + "AttachedToClass": "クラスに接続", + "Name": "名前", + "Description": "説明", + "ShortDescription": "短い説明", + "Descriptor": "識別子", + "TargetClass": "対象クラス", + "Role": "ロール", + "Roles": "ロール一覧", + "Private": "非公開", + "Archived": "アーカイブ済み", + "ClassLabel": "タイプ", + "ClassPropertyLabel": "ラベル", + "String": "テキスト", + "Markup": "マークアップ", + "Number": "数値", + "Boolean": "チェックボックス", + "Timestamp": "タイムスタンプ", + "Date": "日付", + "IntlString": "多言語テキスト", + "Ref": "参照", + "Collection": "コレクション", + "Array": "複数選択", + "Enum": "選択肢", + "Members": "メンバー", + "Hyperlink": "URL", + "MarkupBlobRef": "共同編集", + "Object": "オブジェクト", + "System": "システム", + "CreatedBy": "作成者", + "CreatedDate": "作成日", + "Status": "ステータス", + "StatusCategory": "ステータスカテゴリ", + "Account": "アカウント", + "Rank": "ランク", + "Owners": "所有者", + "Permission": "権限", + "CreateObject": "オブジェクトの作成", + "UpdateObject": "オブジェクトの更新", + "DeleteObject": "オブジェクトの削除", + "ForbidDeleteObject": "オブジェクトの削除禁止", + "UpdateSpace": "スペースの更新", + "ArchiveSpace": "スペースのアーカイブ", + "CreateObjectDescription": "ユーザーにスペース内でオブジェクトを作成する権限を付与します", + "UpdateObjectDescription": "ユーザーにスペース内でオブジェクトを更新する権限を付与します", + "DeleteObjectDescription": "ユーザーにスペース内でオブジェクトを削除する権限を付与します", + "ForbidDeleteObjectDescription": "ユーザーによるスペース内のオブジェクトの削除を禁止します", + "UpdateSpaceDescription": "ユーザーにスペースを更新する権限を付与します", + "ArchiveSpaceDescription": "ユーザーにスペースをアーカイブする権限を付与します", + "AutoJoin": "自動参加", + "AutoJoinDescr": "新しいユーザーを自動的にこのスペースに参加させます", + "BlobSize": "サイズ", + "BlobContentType": "コンテンツタイプ", + "Relation": "関係", + "Relations": "関係一覧", + "AddRelation": "関係を追加", + "PersonId": "人物", + "AccountId": "アカウント" + } +} diff --git a/foundations/core/packages/core/lang/pt.json b/foundations/core/packages/core/lang/pt.json new file mode 100644 index 0000000000..c7df192c7d --- /dev/null +++ b/foundations/core/packages/core/lang/pt.json @@ -0,0 +1,65 @@ +{ + "string": { + "Id": "Id", + "Space": "Espaço", + "Spaces": "Espaços", + "SpacesDescription": "Gestão do tipo de espaço para todas as espaços", + "Modified": "Modificado", + "ModifiedDate": "Data de modificação", + "ModifiedBy": "Modificado por", + "Class": "Classe", + "AttachedTo": "Anexado a", + "AttachedToClass": "Classe anexada", + "Name": "Nome", + "Description": "Descrição", + "Private": "Privado", + "Archived": "Arquivado", + "ClassLabel": "Tipo", + "ClassPropertyLabel": "Rótulo", + "String": "Texto", + "Markup": "Marcação", + "Number": "Número", + "Boolean": "Caixa de seleção", + "Timestamp": "Marca de data/hora", + "Date": "Data", + "IntlString": "Cadeia de texto internacionalizada", + "Ref": "Referência", + "Collection": "Coleção", + "Array": "Seleção múltipla", + "Enum": "Seleção", + "Members": "Membros", + "Hyperlink": "URL", + "MarkupBlobRef": "Colaborativo", + "Object": "Objeto", + "System": "Sistema", + "CreatedBy": "Criado por", + "CreatedDate": "Data de criação", + "Status": "Estado", + "StatusCategory": "Categoria de estado", + "Account": "Conta", + "Rank": "Ranking", + "Owners": "Proprietários", + "Permission": "Permissão", + "CreateObject": "Criar objeto", + "UpdateObject": "Atualizar objeto", + "DeleteObject": "Apagar objeto", + "ForbidDeleteObject": "Proibir apagar objeto", + "UpdateSpace": "Atualizar espaço", + "ArchiveSpace": "Arquivar espaço", + "CreateObjectDescription": "Concede aos usuários a capacidade de criar objetos no espaço", + "UpdateObjectDescription": "Concede aos usuários a capacidade de atualizar objetos no espaço", + "DeleteObjectDescription": "Concede aos usuários a capacidade de apagar objetos no espaço", + "ForbidDeleteObjectDescription": "Proíbe aos usuários a capacidade de apagar objetos no espaço", + "UpdateSpaceDescription": "Concede aos usuários a capacidade de atualizar o espaço", + "ArchiveSpaceDescription": "Concede aos usuários a capacidade de arquivar o espaço", + "AutoJoin": "Auto adesão", + "AutoJoinDescr": "Adesão automática de novos funcionários a este espaço", + "BlobSize": "Tamanho", + "BlobContentType": "Tipo de conteúdo", + "Relation": "Relação", + "Relations": "Relações", + "AddRelation": "Adicionar relação", + "PersonId": "ID de pessoa", + "AccountId": "Conta" + } +} diff --git a/foundations/core/packages/core/lang/ru.json b/foundations/core/packages/core/lang/ru.json new file mode 100644 index 0000000000..24cb255c36 --- /dev/null +++ b/foundations/core/packages/core/lang/ru.json @@ -0,0 +1,72 @@ +{ + "string": { + "Id": "Id", + "Space": "Пространство", + "Spaces": "Пространства", + "SpacesDescription": "Управлять типом пространства всех пространств", + "TypedSpace": "Типизированное пространство", + "SpaceType": "Тип пространства", + "Modified": "Изменено", + "ModifiedDate": "Дата изменения", + "ModifiedBy": "Изменен", + "Class": "Класс", + "AttachedTo": "Прикреплен к", + "AttachedToClass": "Прикреплен к классу", + "Name": "Название", + "Description": "Описание", + "ShortDescription": "Короткое описание", + "Descriptor": "Дескриптор", + "TargetClass": "Целевой класс", + "Role": "Роль", + "Roles": "Роли", + "Private": "Личный", + "Archived": "Архивный", + "ClassLabel": "Тип", + "ClassPropertyLabel": "Название", + "String": "Текст", + "Markup": "Разметка", + "Number": "Число", + "Boolean": "Чекбокс", + "Timestamp": "Временная отметка", + "Date": "Дата", + "IntlString": "Интернационализированная строка", + "Ref": "Ссылка", + "Collection": "Коллекция", + "Array": "Множественный выбор", + "Enum": "Выбор", + "Members": "Участники", + "Hyperlink": "URL", + "MarkupBlobRef": "Коллаборативный", + "Object": "Объект", + "System": "Система", + "CreatedBy": "Создан", + "CreatedDate": "Дата создания", + "Status": "Статус", + "StatusCategory": "Категория статуса", + "Account": "Аккаунт", + "Rank": "Ранг", + "Owners": "Владельцы", + "Permission": "Разрешение", + "CreateObject": "Создавать объект", + "UpdateObject": "Обновлять объект", + "DeleteObject": "Удалять объект", + "ForbidDeleteObject": "Запретить удалять объект", + "UpdateSpace": "Обновлять пространство", + "ArchiveSpace": "Архивировать пространство", + "CreateObjectDescription": "Дает пользователям разрешение создавать объекты в пространстве", + "UpdateObjectDescription": "Дает пользователям разрешение обновлять объекты в пространстве", + "DeleteObjectDescription": "Дает пользователям разрешение удалять объекты в пространстве", + "ForbidDeleteObjectDescription": "Запрещает пользователям удалять объекты в пространстве", + "UpdateSpaceDescription": "Дает пользователям разрешение обновлять пространство", + "ArchiveSpaceDescription": "Дает пользователям разрешение архивировать пространство", + "AutoJoin": "Автоприсоединение", + "AutoJoinDescr": "Автоматически присоединять новых сотрудников к этому пространству", + "BlobSize": "Размер", + "BlobContentType": "Тип контента", + "Relation": "Связь", + "Relations": "Связи", + "AddRelation": "Добавить связь", + "PersonId": "Персона", + "AccountId": "Аккаунт" + } +} diff --git a/foundations/core/packages/core/lang/tr.json b/foundations/core/packages/core/lang/tr.json new file mode 100644 index 0000000000..1f862d2e08 --- /dev/null +++ b/foundations/core/packages/core/lang/tr.json @@ -0,0 +1,72 @@ +{ + "string": { + "Id": "Id", + "Space": "Alan", + "Spaces": "Alanlar", + "SpacesDescription": "Tüm alanların alan tipini yönet", + "TypedSpace": "Tipli alan", + "SpaceType": "Alan tipi", + "Modified": "Değiştirildi", + "ModifiedDate": "Değiştirilme tarihi", + "ModifiedBy": "Değiştiren", + "Class": "Sınıf", + "AttachedTo": "Eklenen", + "AttachedToClass": "Eklenen sınıf", + "Name": "İsim", + "Description": "Açıklama", + "ShortDescription": "Kısa açıklama", + "Descriptor": "Tanımlayıcı", + "TargetClass": "Hedef sınıf", + "Role": "Rol", + "Roles": "Roller", + "Private": "Özel", + "Archived": "Arşivlendi", + "ClassLabel": "Tip", + "ClassPropertyLabel": "Etiket", + "String": "Metin", + "Markup": "İşaretleme", + "Number": "Sayı", + "Boolean": "Onay kutusu", + "Timestamp": "Zaman damgası", + "Date": "Tarih", + "IntlString": "IntlString", + "Ref": "Referans", + "Collection": "Koleksiyon", + "Array": "Çoklu Seçim", + "Enum": "Seçim", + "Members": "Üyeler", + "Hyperlink": "URL", + "MarkupBlobRef": "İşbirlikçi", + "Object": "Nesne", + "System": "Sistem", + "CreatedBy": "Oluşturan", + "CreatedDate": "Oluşturulma tarihi", + "Status": "Durum", + "StatusCategory": "Durum kategorisi", + "Account": "Hesap", + "Rank": "Sıra", + "Owners": "Sahipler", + "Permission": "İzin", + "CreateObject": "Nesne oluştur", + "UpdateObject": "Nesneyi güncelle", + "DeleteObject": "Nesneyi sil", + "ForbidDeleteObject": "Nesne silmeyi yasakla", + "UpdateSpace": "Alanı güncelle", + "ArchiveSpace": "Alanı arşivle", + "CreateObjectDescription": "Kullanıcılara alanda nesne oluşturma yetkisi verir", + "UpdateObjectDescription": "Kullanıcılara alandaki nesneleri güncelleme yetkisi verir", + "DeleteObjectDescription": "Kullanıcılara alandaki nesneleri silme yetkisi verir", + "ForbidDeleteObjectDescription": "Kullanıcıların alandaki nesneleri silmesini yasaklar", + "UpdateSpaceDescription": "Kullanıcılara alanı güncelleme yetkisi verir", + "ArchiveSpaceDescription": "Kullanıcılara alanı arşivleme yetkisi verir", + "AutoJoin": "Otomatik katıl", + "AutoJoinDescr": "Yeni çalışanları bu alana otomatik olarak ekle", + "BlobSize": "Boyut", + "BlobContentType": "İçerik türü", + "Relation": "İlişki", + "Relations": "İlişkiler", + "AddRelation": "İlişki ekle", + "PersonId": "Kişi", + "AccountId": "Hesap" + } +} diff --git a/foundations/core/packages/core/lang/zh.json b/foundations/core/packages/core/lang/zh.json new file mode 100644 index 0000000000..696a334f60 --- /dev/null +++ b/foundations/core/packages/core/lang/zh.json @@ -0,0 +1,72 @@ +{ + "string": { + "Id": "Id", + "Space": "空间", + "Spaces": "空间", + "SpacesDescription": "管理所有空间的空间类型", + "TypedSpace": "类型化空间", + "SpaceType": "空间类型", + "Modified": "已修改", + "ModifiedDate": "修改日期", + "ModifiedBy": "修改者", + "Class": "类", + "AttachedTo": "附加到", + "AttachedToClass": "附加到类", + "Name": "名称", + "Description": "描述", + "ShortDescription": "简短描述", + "Descriptor": "描述符", + "TargetClass": "目标类", + "Role": "角色", + "Roles": "角色", + "Private": "私有", + "Archived": "已归档", + "ClassLabel": "类型", + "ClassPropertyLabel": "标签", + "String": "文字", + "Markup": "标记", + "Number": "数字", + "Boolean": "复选框", + "Timestamp": "时间戳", + "Date": "日期", + "IntlString": "国际字符串", + "Ref": "引用", + "Collection": "集合", + "Array": "多选", + "Enum": "选择", + "Members": "成员", + "Hyperlink": "URL", + "MarkupBlobRef": "协作", + "Object": "对象", + "System": "系统", + "CreatedBy": "创建者", + "CreatedDate": "创建日期", + "Status": "状态", + "StatusCategory": "状态类别", + "Account": "账户", + "Rank": "排名", + "Owners": "所有者", + "Permission": "权限", + "CreateObject": "创建对象", + "UpdateObject": "更新对象", + "DeleteObject": "删除对象", + "ForbidDeleteObject": "禁止删除对象", + "UpdateSpace": "更新空间", + "ArchiveSpace": "归档空间", + "CreateObjectDescription": "授予用户在空间中创建对象的权限", + "UpdateObjectDescription": "授予用户在空间中更新对象的权限", + "DeleteObjectDescription": "授予用户在空间中删除对象的权限", + "ForbidDeleteObjectDescription": "禁止用户在空间中删除对象", + "UpdateSpaceDescription": "授予用户更新空间的权限", + "ArchiveSpaceDescription": "授予用户归档空间的权限", + "AutoJoin": "自动加入", + "AutoJoinDescr": "自动将新员工加入此空间", + "BlobSize": "大小", + "BlobContentType": "內容類型", + "Relation": "关系", + "Relations": "关系", + "AddRelation": "添加关系", + "PersonId": "人员 ID", + "AccountId": "帐户" + } +} diff --git a/foundations/core/packages/core/package.json b/foundations/core/packages/core/package.json new file mode 100644 index 0000000000..2e851ff81b --- /dev/null +++ b/foundations/core/packages/core/package.json @@ -0,0 +1,72 @@ +{ + "name": "@hcengineering/core", + "version": "0.7.22", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "!lib/**/__test__/**", + "types/**/*", + "!types/**/__test__/**", + "src/**/*", + "!src/**/__test__/**", + "lang/**/*", + "README.md", + "CHANGELOG.md" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/node": "^22.18.1", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/platform": "workspace:^0.7.18", + "@hcengineering/analytics": "workspace:^0.7.17", + "@hcengineering/measurements": "workspace:^0.7.18", + "fast-equals": "^5.2.2" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + }, + "./lang/*.json": { + "require": "./lang/*.json", + "import": "./lang/*.json" + }, + "./lang": { + "require": "./lang", + "import": "./lang" + } + } +} diff --git a/foundations/core/packages/core/src/__tests__/client.test.ts b/foundations/core/packages/core/src/__tests__/client.test.ts new file mode 100644 index 0000000000..ae01a11545 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/client.test.ts @@ -0,0 +1,254 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021, 2022 Hardcore Engineering, Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { type IntlString, type Plugin } from '@hcengineering/platform' +import { ClientConnectEvent, type DocChunk } from '..' +import type { Class, Data, Doc, Domain, PluginConfiguration, Ref, Space, Timestamp } from '../classes' +import { ClassifierKind, DOMAIN_MODEL } from '../classes' +import { type ClientConnection, createClient } from '../client' +import core from '../component' +import { Hierarchy } from '../hierarchy' +import { ModelDb, TxDb } from '../memdb' +import { TxOperations } from '../operations' +import type { + DocumentQuery, + DomainResult, + FindResult, + SearchOptions, + SearchQuery, + SearchResult, + TxResult +} from '../storage' +import { type Tx, TxFactory, TxProcessor } from '../tx' +import { fillConfiguration, generateId, pluginFilterTx } from '../utils' +import { connect } from './connection' +import { genMinModel } from './minmodel' + +function filterPlugin (plugin: Plugin): (txes: Tx[]) => Tx[] { + return (txes) => { + const configs = new Map, PluginConfiguration>() + fillConfiguration(txes, configs) + + const excludedPlugins = Array.from(configs.values()).filter((it) => !it.enabled || it.pluginId !== plugin) + return pluginFilterTx(excludedPlugins, configs, txes) + } +} + +describe('client', () => { + it('should create client and spaces', async () => { + const klass = core.class.Space + const client = new TxOperations(await createClient(connect), core.account.System) + const result = await client.findAll(klass, {}) + expect(result).toHaveLength(2) + + await client.createDoc(klass, core.space.Model, { + private: false, + name: 'NewSpace', + description: '', + archived: false, + members: [] + }) + const result2 = await client.findAll(klass, {}) + expect(result2).toHaveLength(3) + + await client.createDoc(klass, core.space.Model, { + private: false, + name: 'NewSpace', + description: '', + members: [], + archived: false + }) + const result3 = await client.findAll(klass, {}) + expect(result3).toHaveLength(4) + + const result4 = await client.findOne(klass, {}) + expect(result4).toEqual(result3[0]) + }) + + it('should create client with plugins', async () => { + const txFactory = new TxFactory(core.account.System) + const txes = genMinModel() + + txes.push( + txFactory.createTxCreateDoc( + core.class.Class, + core.space.Model, + { + label: 'PluginConfiguration' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }, + core.class.PluginConfiguration + ) + ) + + async function connectPlugin (handler: (tx: Tx) => void): Promise { + const hierarchy = new Hierarchy() + + for (const tx of txes) hierarchy.tx(tx) + + const transactions = new TxDb(hierarchy) + const model = new ModelDb(hierarchy) + for (const tx of txes) { + await transactions.tx(tx) + await model.tx(tx) + } + + async function findAll (_class: Ref>, query: DocumentQuery): Promise> { + return await transactions.findAll(_class, query) + } + + return new (class implements ClientConnection { + handler?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise + + set onConnect ( + handler: ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined + ) { + this.handler = handler + void this.handler?.(ClientConnectEvent.Connected, '', {}) + } + + get onConnect (): + | ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) + | undefined { + return this.handler + } + + isConnected = (): boolean => true + findAll = findAll + pushHandler = (): void => {} + + domainRequest (): Promise { + return Promise.resolve({ domain: 'test' as Domain, value: null }) + } + + searchFulltext = async (query: SearchQuery, options: SearchOptions): Promise => { + return { docs: [] } + } + + tx = async (tx: Tx): Promise => { + if (tx.objectSpace === core.space.Model) { + hierarchy.tx(tx) + } + const result = await Promise.all([transactions.tx(tx)]) + return result[0] + } + + close = async (): Promise => {} + + loadChunk = async (domain: Domain, idx?: number): Promise => ({ + idx: -1, + docs: [], + finished: true + }) + + async getDomainHash (domain: Domain): Promise { + return generateId() + } + + async closeChunk (idx: number): Promise {} + async loadDocs (domain: Domain, docs: Ref[]): Promise { + return [] + } + + async upload (domain: Domain, docs: Doc[]): Promise {} + async clean (domain: Domain, docs: Ref[]): Promise {} + async loadModel (last: Timestamp): Promise { + return txes + } + + async sendForceClose (): Promise {} + })() + } + const spyCreate = jest.spyOn(TxProcessor, 'createDoc2Doc') + const spyUpdate = jest.spyOn(TxProcessor, 'updateDoc2Doc') + + const pluginData1: Data = { + pluginId: 'testPlugin1' as Plugin, + label: 'Test Plugin 1' as IntlString, + transactions: [], + beta: true, + enabled: true + } + const txCreateDoc1 = txFactory.createTxCreateDoc(core.class.PluginConfiguration, core.space.Model, pluginData1) + txes.push(txCreateDoc1) + const client1 = new TxOperations( + await createClient(connectPlugin, filterPlugin('testPlugin1' as Plugin)), + core.account.System + ) + const result1 = await client1.findAll(core.class.PluginConfiguration, {}) + + expect(result1).toHaveLength(1) + expect(result1[0]._id).toStrictEqual(txCreateDoc1.objectId) + expect(spyCreate).toHaveBeenLastCalledWith(txCreateDoc1, false) + expect(spyUpdate).toHaveBeenCalledTimes(0) + await client1.close() + + const pluginData2 = { + pluginId: 'testPlugin2' as Plugin, + label: 'Test Plugin 2' as IntlString, + transactions: [], + beta: true, + enabled: true + } + const txCreateDoc2 = txFactory.createTxCreateDoc(core.class.PluginConfiguration, core.space.Model, pluginData2) + txes.push(txCreateDoc2) + const client2 = new TxOperations( + await createClient(connectPlugin, filterPlugin('testPlugin1' as Plugin)), + core.account.System + ) + const result2 = await client2.findAll(core.class.PluginConfiguration, {}) + + expect(result2).toHaveLength(2) + expect(result2[0]._id).toStrictEqual(txCreateDoc1.objectId) + expect(result2[1]._id).toStrictEqual(txCreateDoc2.objectId) + expect(spyCreate).toHaveBeenLastCalledWith(txCreateDoc2, false) + expect(spyUpdate).toHaveBeenCalledTimes(0) + await client2.close() + + const pluginData3 = { + pluginId: 'testPlugin3' as Plugin, + label: 'Test Plugin 3' as IntlString, + transactions: [txCreateDoc1._id], + beta: true, + enabled: true + } + const txUpdateDoc = txFactory.createTxUpdateDoc( + core.class.PluginConfiguration, + core.space.Model, + txCreateDoc1.objectId, + pluginData3 + ) + txes.push(txUpdateDoc) + const client3 = new TxOperations( + await createClient(connectPlugin, filterPlugin('testPlugin2' as Plugin)), + core.account.System + ) + const result3 = await client3.findAll(core.class.PluginConfiguration, {}) + + expect(result3).toHaveLength(1) + expect(result3[0]._id).toStrictEqual(txCreateDoc2.objectId) + expect(spyCreate).toHaveBeenLastCalledWith(txCreateDoc2, false) + expect(spyUpdate.mock.calls[1][1]).toStrictEqual(txUpdateDoc) + expect(spyUpdate).toBeCalledTimes(2) + await client3.close() + + spyCreate.mockReset() + spyCreate.mockRestore() + spyUpdate.mockReset() + spyUpdate.mockRestore() + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/clone.test.ts b/foundations/core/packages/core/src/__tests__/clone.test.ts new file mode 100644 index 0000000000..66d7e651da --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/clone.test.ts @@ -0,0 +1,83 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { clone, getTypeOf } from '../clone' + +describe('clone', () => { + it('should handle primitive types', () => { + expect(clone(undefined)).toBeUndefined() + expect(clone(null)).toBeNull() + expect(clone(123)).toBe(123) + expect(clone('test')).toBe('test') + expect(clone(true)).toBe(true) + }) + + it('should clone Date objects', () => { + const date = new Date() + const cloned = clone(date) + expect(cloned).toBeInstanceOf(Date) + expect(cloned.getTime()).toBe(date.getTime()) + }) + + it('should clone Arrays', () => { + const arr = [1, 'test', { a: 1 }, new Date()] + const cloned = clone(arr) + expect(cloned).toEqual(arr) + expect(cloned).not.toBe(arr) + expect(cloned[2]).not.toBe(arr[2]) + expect(cloned[3]).not.toBe(arr[3]) + }) + + it('should clone Objects', () => { + const obj = { + num: 123, + str: 'test', + date: new Date(), + nested: { a: 1 }, + arr: [1, 2, 3] + } + const cloned = clone(obj) + expect(cloned).toEqual(obj) + expect(cloned).not.toBe(obj) + expect(cloned.nested).not.toBe(obj.nested) + expect(cloned.arr).not.toBe(obj.arr) + expect(cloned.date).not.toBe(obj.date) + }) + + it('should respect depth parameter', () => { + const deep = { a: { b: { c: { d: 1 } } } } + expect(clone(deep, undefined, undefined, 0)).toBe(deep) + expect(clone(deep, undefined, undefined, 1)).toEqual({ a: { b: { c: { d: 1 } } } }) + expect(clone(deep, undefined, undefined, 2)).toEqual({ a: { b: { c: { d: 1 } } } }) + expect(clone(deep, undefined, undefined, 3)).toEqual({ a: { b: { c: { d: 1 } } } }) + }) + + it('should handle functions', () => { + const fn: () => any = () => {} + expect(clone(fn)).toBe(fn) + }) +}) + +describe('getTypeOf', () => { + it('should detect types correctly', () => { + expect(getTypeOf(null)).toBe('null') + expect(getTypeOf([])).toBe('Array') + expect(getTypeOf({})).toBe('Object') + expect(getTypeOf(new Date())).toBe('Date') + expect(getTypeOf(/test/)).toBe('RegExp') + expect(getTypeOf(123)).toBe('number') + expect(getTypeOf('test')).toBe('string') + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/collaboration.test.ts b/foundations/core/packages/core/src/__tests__/collaboration.test.ts new file mode 100644 index 0000000000..9a8fbaa964 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/collaboration.test.ts @@ -0,0 +1,310 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + makeCollabId, + makeDocCollabId, + makeCollabYdocId, + makeCollabJsonId, + type CollaborativeDoc +} from '../collaboration' +import type { Class, Doc, Ref } from '../classes' + +describe('collaboration', () => { + describe('makeCollabId', () => { + it('should create collaborative doc id from parameters', () => { + const objectClass = 'class:core.Doc' as Ref> + const objectId = 'doc123' as Ref + const objectAttr = 'content' + + const result = makeCollabId(objectClass, objectId, objectAttr) + + expect(result).toEqual({ + objectClass, + objectId, + objectAttr + }) + }) + + it('should handle different attribute types', () => { + const objectClass = 'class:task.Task' as Ref> + const objectId = 'task456' as Ref + + const result1 = makeCollabId(objectClass, objectId, 'description') + const result2 = makeCollabId(objectClass, objectId, 'comments') + + expect(result1.objectAttr).toBe('description') + expect(result2.objectAttr).toBe('comments') + }) + + it('should create unique ids for same object but different attributes', () => { + const objectClass = 'class:core.Doc' as Ref> + const objectId = 'doc123' as Ref + + const result1 = makeCollabId(objectClass, objectId, 'attr1') + const result2 = makeCollabId(objectClass, objectId, 'attr2') + + expect(result1.objectAttr).not.toBe(result2.objectAttr) + }) + }) + + describe('makeDocCollabId', () => { + it('should create collaborative doc id from document', () => { + const doc: Doc = { + _id: 'doc789' as Ref, + _class: 'class:core.Doc' as Ref>, + space: 'space1' as any, + modifiedOn: 12345, + modifiedBy: 'user1' as any + } + + const result = makeDocCollabId(doc, 'content') + + expect(result).toEqual({ + objectClass: doc._class, + objectId: doc._id, + objectAttr: 'content' + }) + }) + + it('should work with typed attribute keys', () => { + interface TestDoc extends Doc { + title: string + description: string + } + + const doc: TestDoc = { + _id: 'doc999' as Ref, + _class: 'class:test.TestDoc' as Ref>, + space: 'space1' as any, + modifiedOn: 12345, + modifiedBy: 'user1' as any, + title: 'Test', + description: 'Description' + } + + const result = makeDocCollabId(doc, 'description') + + expect(result.objectAttr).toBe('description') + }) + + it('should handle documents with complex ids', () => { + const doc: Doc = { + _id: 'space:task:project-1:task-123' as Ref, + _class: 'class:task.Task' as Ref>, + space: 'space1' as any, + modifiedOn: 12345, + modifiedBy: 'user1' as any + } + + const result = makeDocCollabId(doc, 'content') + + expect(result.objectId).toBe('space:task:project-1:task-123') + }) + }) + + describe('makeCollabYdocId', () => { + it('should create ydoc id from collaborative doc', () => { + const collabDoc: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'doc123' as Ref, + objectAttr: 'content' + } + + const result = makeCollabYdocId(collabDoc) + + expect(result).toBe('doc123%content') + }) + + it('should use % as separator', () => { + const collabDoc: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'myDoc' as Ref, + objectAttr: 'myAttr' + } + + const result = makeCollabYdocId(collabDoc) + + expect(result).toContain('%') + expect(result.split('%')).toHaveLength(2) + }) + + it('should create consistent ids', () => { + const collabDoc: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'doc456' as Ref, + objectAttr: 'field' + } + + const result1 = makeCollabYdocId(collabDoc) + const result2 = makeCollabYdocId(collabDoc) + + expect(result1).toBe(result2) + }) + + it('should create different ids for different attributes', () => { + const collabDoc1: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'doc789' as Ref, + objectAttr: 'content' + } + + const collabDoc2: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'doc789' as Ref, + objectAttr: 'description' + } + + const result1 = makeCollabYdocId(collabDoc1) + const result2 = makeCollabYdocId(collabDoc2) + + expect(result1).not.toBe(result2) + }) + + it('should handle special characters in ids', () => { + const collabDoc: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'doc:with:colons' as Ref, + objectAttr: 'attr-with-dash' + } + + const result = makeCollabYdocId(collabDoc) + + expect(result).toBe('doc:with:colons%attr-with-dash') + }) + }) + + describe('makeCollabJsonId', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should create json id with timestamp', () => { + const mockDate = new Date('2024-01-01T00:00:00.000Z') + jest.setSystemTime(mockDate) + + const collabDoc: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'doc123' as Ref, + objectAttr: 'content' + } + + const result = makeCollabJsonId(collabDoc) + + expect(result).toBe(`doc123-content-${mockDate.getTime()}`) + }) + + it('should use - as separator', () => { + const collabDoc: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'myDoc' as Ref, + objectAttr: 'myAttr' + } + + const result = makeCollabJsonId(collabDoc) + + const parts = result.split('-') + expect(parts.length).toBeGreaterThanOrEqual(3) + expect(parts[0]).toBe('myDoc') + expect(parts[1]).toBe('myAttr') + }) + + it('should create different ids when called at different times', () => { + const collabDoc: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'doc456' as Ref, + objectAttr: 'field' + } + + jest.setSystemTime(new Date('2024-01-01T00:00:00.000Z')) + const result1 = makeCollabJsonId(collabDoc) + + jest.setSystemTime(new Date('2024-01-01T00:00:01.000Z')) + const result2 = makeCollabJsonId(collabDoc) + + expect(result1).not.toBe(result2) + }) + + it('should include all three components', () => { + const mockDate = new Date('2024-06-15T12:30:45.000Z') + jest.setSystemTime(mockDate) + + const collabDoc: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'testDoc' as Ref, + objectAttr: 'testAttr' + } + + const result = makeCollabJsonId(collabDoc) + + expect(result).toContain('testDoc') + expect(result).toContain('testAttr') + expect(result).toContain(mockDate.getTime().toString()) + }) + + it('should handle objects with ids containing dashes', () => { + const mockDate = new Date('2024-01-01T00:00:00.000Z') + jest.setSystemTime(mockDate) + + const collabDoc: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'doc-with-dashes' as Ref, + objectAttr: 'attr-with-dashes' + } + + const result = makeCollabJsonId(collabDoc) + + expect(result).toBe(`doc-with-dashes-attr-with-dashes-${mockDate.getTime()}`) + }) + }) + + describe('integration tests', () => { + it('should create different types of ids from same collaborative doc', () => { + const collabDoc: CollaborativeDoc = { + objectClass: 'class:core.Doc' as Ref>, + objectId: 'doc123' as Ref, + objectAttr: 'content' + } + + const ydocId = makeCollabYdocId(collabDoc) + const jsonId = makeCollabJsonId(collabDoc) + + expect(ydocId).not.toBe(jsonId) + expect(ydocId).toContain(collabDoc.objectId) + expect(jsonId).toContain(collabDoc.objectId) + }) + + it('should work with makeDocCollabId and other functions', () => { + const doc: Doc = { + _id: 'doc999' as Ref, + _class: 'class:core.Doc' as Ref>, + space: 'space1' as any, + modifiedOn: 12345, + modifiedBy: 'user1' as any + } + + const collabDoc = makeDocCollabId(doc, 'content') + const ydocId = makeCollabYdocId(collabDoc) + const jsonId = makeCollabJsonId(collabDoc) + + expect(ydocId).toContain('doc999') + expect(jsonId).toContain('doc999') + }) + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/collaborators.test.ts b/foundations/core/packages/core/src/__tests__/collaborators.test.ts new file mode 100644 index 0000000000..3e69795765 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/collaborators.test.ts @@ -0,0 +1,201 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { getClassCollaborators } from '../collaborators' +import { ModelDb } from '../memdb' +import { Hierarchy } from '../hierarchy' +import core from '../component' +import type { Class, ClassCollaborators, Doc, Ref } from '../classes' + +describe('collaborators', () => { + let model: ModelDb + let hierarchy: Hierarchy + + beforeEach(() => { + model = new ModelDb(hierarchy) + hierarchy = new Hierarchy() + }) + + describe('getClassCollaborators', () => { + it('should return undefined when no collaborators found', () => { + const classRef = 'class:test.TestClass' as Ref> + + // Mock hierarchy to return empty ancestors + hierarchy.getAncestors = jest.fn().mockReturnValue([classRef]) + + // Mock model to return empty result + model.findAllSync = jest.fn().mockReturnValue([]) + + const result = getClassCollaborators(model, hierarchy, classRef) + + expect(result).toBeUndefined() + }) + + it('should return collaborators for direct class', () => { + const classRef = 'class:test.TestClass' as Ref> + const collaborators: ClassCollaborators = { + _id: 'collab1' as any, + _class: core.class.ClassCollaborators, + space: 'space1' as any, + modifiedOn: Date.now(), + modifiedBy: 'user1' as any, + attachedTo: classRef, + attachedToClass: core.class.Class, + collection: 'collaborators' + } as unknown as ClassCollaborators + + hierarchy.getAncestors = jest.fn().mockReturnValue([classRef]) + model.findAllSync = jest.fn().mockReturnValue([collaborators]) + + const result = getClassCollaborators(model, hierarchy, classRef) + + expect(result).toBe(collaborators) + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(model.findAllSync).toHaveBeenCalledWith(core.class.ClassCollaborators, { attachedTo: { $in: [classRef] } }) + }) + + it('should return collaborators from ancestor class', () => { + const childClass = 'class:test.ChildClass' as Ref> + const parentClass = 'class:test.ParentClass' as Ref> + const grandParentClass = core.class.Doc + + const parentCollaborators: ClassCollaborators = { + _id: 'collab2' as any, + _class: core.class.ClassCollaborators, + space: 'space1' as any, + modifiedOn: Date.now(), + modifiedBy: 'user1' as any, + attachedTo: parentClass, + attachedToClass: core.class.Class, + collection: 'collaborators' + } as unknown as ClassCollaborators + + hierarchy.getAncestors = jest.fn().mockReturnValue([childClass, parentClass, grandParentClass]) + model.findAllSync = jest.fn().mockReturnValue([parentCollaborators]) + + const result = getClassCollaborators(model, hierarchy, childClass) + + expect(result).toBe(parentCollaborators) + }) + + it('should return first matching ancestor collaborators', () => { + const childClass = 'class:test.ChildClass' as Ref> + const parentClass = 'class:test.ParentClass' as Ref> + const grandParentClass = 'class:test.GrandParentClass' as Ref> + + const parentCollaborators: ClassCollaborators = { + _id: 'collab3' as any, + _class: core.class.ClassCollaborators, + space: 'space1' as any, + modifiedOn: Date.now(), + modifiedBy: 'user1' as any, + attachedTo: parentClass, + attachedToClass: core.class.Class, + collection: 'collaborators' + } as unknown as ClassCollaborators + + const grandParentCollaborators: ClassCollaborators = { + _id: 'collab4' as any, + _class: core.class.ClassCollaborators, + space: 'space1' as any, + modifiedOn: Date.now(), + modifiedBy: 'user1' as any, + attachedTo: grandParentClass, + attachedToClass: core.class.Class, + collection: 'collaborators' + } as unknown as ClassCollaborators + + hierarchy.getAncestors = jest.fn().mockReturnValue([childClass, parentClass, grandParentClass]) + model.findAllSync = jest.fn().mockReturnValue([parentCollaborators, grandParentCollaborators]) + + const result = getClassCollaborators(model, hierarchy, childClass) + + // Should return parent collaborators (first in ancestor chain) + expect(result).toBe(parentCollaborators) + }) + + it('should handle single class with no ancestors', () => { + const classRef = core.class.Doc + + hierarchy.getAncestors = jest.fn().mockReturnValue([classRef]) + model.findAllSync = jest.fn().mockReturnValue([]) + + const result = getClassCollaborators(model, hierarchy, classRef) + + expect(result).toBeUndefined() + }) + + it('should handle empty ancestors list', () => { + const classRef = 'class:test.TestClass' as Ref> + + hierarchy.getAncestors = jest.fn().mockReturnValue([]) + model.findAllSync = jest.fn().mockReturnValue([]) + + const result = getClassCollaborators(model, hierarchy, classRef) + + expect(result).toBeUndefined() + }) + + it('should properly query model with $in operator', () => { + const childClass = 'class:test.ChildClass' as Ref> + const parentClass = 'class:test.ParentClass' as Ref> + const ancestors = [childClass, parentClass, core.class.Doc] + + hierarchy.getAncestors = jest.fn().mockReturnValue(ancestors) + model.findAllSync = jest.fn().mockReturnValue([]) + + getClassCollaborators(model, hierarchy, childClass) + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(model.findAllSync).toHaveBeenCalledWith(core.class.ClassCollaborators, { attachedTo: { $in: ancestors } }) + }) + + it('should iterate through ancestors in order', () => { + const class1 = 'class:test.Class1' as Ref> + const class2 = 'class:test.Class2' as Ref> + const class3 = 'class:test.Class3' as Ref> + + const collab2: ClassCollaborators = { + _id: 'collab5' as any, + _class: core.class.ClassCollaborators, + space: 'space1' as any, + modifiedOn: Date.now(), + modifiedBy: 'user1' as any, + attachedTo: class2, + attachedToClass: core.class.Class, + collection: 'collaborators' + } as unknown as ClassCollaborators + + const collab3: ClassCollaborators = { + _id: 'collab6' as any, + _class: core.class.ClassCollaborators, + space: 'space1' as any, + modifiedOn: Date.now(), + modifiedBy: 'user1' as any, + attachedTo: class3, + attachedToClass: core.class.Class, + collection: 'collaborators' + } as unknown as ClassCollaborators + + hierarchy.getAncestors = jest.fn().mockReturnValue([class1, class2, class3]) + model.findAllSync = jest.fn().mockReturnValue([collab2, collab3]) + + const result = getClassCollaborators(model, hierarchy, class1) + + // Should return collab2 (class2 comes before class3 in ancestors) + expect(result).toBe(collab2) + }) + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/common.test.ts b/foundations/core/packages/core/src/__tests__/common.test.ts new file mode 100644 index 0000000000..32caf9e481 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/common.test.ts @@ -0,0 +1,283 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { groupByArray, groupByArrayAsync, flipSet } from '../common' + +describe('common utilities', () => { + describe('groupByArray', () => { + it('should group array items by key', () => { + const items = [ + { id: 1, category: 'A' }, + { id: 2, category: 'B' }, + { id: 3, category: 'A' }, + { id: 4, category: 'C' }, + { id: 5, category: 'B' } + ] + + const result = groupByArray(items, (item) => item.category) + + expect(result.size).toBe(3) + expect(result.get('A')).toEqual([ + { id: 1, category: 'A' }, + { id: 3, category: 'A' } + ]) + expect(result.get('B')).toEqual([ + { id: 2, category: 'B' }, + { id: 5, category: 'B' } + ]) + expect(result.get('C')).toEqual([{ id: 4, category: 'C' }]) + }) + + it('should handle empty array', () => { + const result = groupByArray([], (item) => item) + expect(result.size).toBe(0) + }) + + it('should handle single item', () => { + const items = [{ id: 1, type: 'test' }] + const result = groupByArray(items, (item) => item.type) + + expect(result.size).toBe(1) + expect(result.get('test')).toEqual([{ id: 1, type: 'test' }]) + }) + + it('should handle numeric keys', () => { + const items = [ + { value: 10, bucket: 1 }, + { value: 20, bucket: 2 }, + { value: 30, bucket: 1 }, + { value: 40, bucket: 3 } + ] + + const result = groupByArray(items, (item) => item.bucket) + + expect(result.size).toBe(3) + expect(result.get(1)).toHaveLength(2) + expect(result.get(2)).toHaveLength(1) + expect(result.get(3)).toHaveLength(1) + }) + + it('should handle all items with same key', () => { + const items = [ + { id: 1, status: 'active' }, + { id: 2, status: 'active' }, + { id: 3, status: 'active' } + ] + + const result = groupByArray(items, (item) => item.status) + + expect(result.size).toBe(1) + expect(result.get('active')).toHaveLength(3) + }) + + it('should handle complex key providers', () => { + const items = [ + { firstName: 'John', lastName: 'Doe' }, + { firstName: 'Jane', lastName: 'Doe' }, + { firstName: 'John', lastName: 'Smith' } + ] + + const result = groupByArray(items, (item) => item.lastName) + + expect(result.size).toBe(2) + expect(result.get('Doe')).toHaveLength(2) + expect(result.get('Smith')).toHaveLength(1) + }) + + it('should handle undefined keys', () => { + const items = [{ id: 1, tag: 'a' }, { id: 2 }, { id: 3, tag: 'a' }] + + const result = groupByArray(items, (item: any) => item.tag) + + expect(result.size).toBe(2) + expect(result.get('a')).toHaveLength(2) + expect(result.get(undefined)).toHaveLength(1) + }) + }) + + describe('groupByArrayAsync', () => { + it('should group array items by async key provider', async () => { + const items = [ + { id: 1, value: 10 }, + { id: 2, value: 20 }, + { id: 3, value: 15 }, + { id: 4, value: 25 } + ] + + const result = await groupByArrayAsync(items, async (item) => { + // Simulate async operation + await new Promise((resolve) => setTimeout(resolve, 1)) + return item.value > 15 ? 'high' : 'low' + }) + + expect(result.size).toBe(2) + expect(result.get('low')).toEqual([ + { id: 1, value: 10 }, + { id: 3, value: 15 } + ]) + expect(result.get('high')).toEqual([ + { id: 2, value: 20 }, + { id: 4, value: 25 } + ]) + }) + + it('should handle empty array', async () => { + const result = await groupByArrayAsync([], async (item) => item) + expect(result.size).toBe(0) + }) + + it('should handle async errors', async () => { + const items = [{ id: 1 }, { id: 2 }] + + await expect( + groupByArrayAsync(items, async (item) => { + if (item.id === 2) { + throw new Error('Async error') + } + return 'key' + }) + ).rejects.toThrow('Async error') + }) + + it('should handle single item', async () => { + const items = [{ id: 1, type: 'test' }] + const result = await groupByArrayAsync(items, async (item) => { + await new Promise((resolve) => setTimeout(resolve, 1)) + return item.type + }) + + expect(result.size).toBe(1) + expect(result.get('test')).toEqual([{ id: 1, type: 'test' }]) + }) + + it('should process items sequentially', async () => { + const order: number[] = [] + const items = [1, 2, 3, 4] + + await groupByArrayAsync(items, async (item) => { + order.push(item) + await new Promise((resolve) => setTimeout(resolve, 5 - item)) // Reverse delays + return item % 2 === 0 ? 'even' : 'odd' + }) + + // Should maintain order despite different delays + expect(order).toEqual([1, 2, 3, 4]) + }) + + it('should handle Promise rejections gracefully', async () => { + const items = [1, 2, 3] + + await expect( + groupByArrayAsync(items, async (item) => { + if (item === 2) { + return await Promise.reject(new Error('Failed on 2')) + } + return 'key' + }) + ).rejects.toThrow('Failed on 2') + }) + + it('should handle numeric keys', async () => { + const items = [{ value: 10 }, { value: 20 }, { value: 30 }] + + const result = await groupByArrayAsync(items, async (item) => { + await new Promise((resolve) => setTimeout(resolve, 1)) + return Math.floor(item.value / 10) + }) + + expect(result.size).toBe(3) + expect(result.get(1)).toHaveLength(1) + expect(result.get(2)).toHaveLength(1) + expect(result.get(3)).toHaveLength(1) + }) + }) + + describe('flipSet', () => { + it('should add item if not present', () => { + const set = new Set([1, 2, 3]) + const result = flipSet(set, 4) + + expect(result).toBe(set) // Should return same set + expect(result.has(4)).toBe(true) + expect(result.size).toBe(4) + }) + + it('should remove item if present', () => { + const set = new Set([1, 2, 3]) + const result = flipSet(set, 2) + + expect(result).toBe(set) // Should return same set + expect(result.has(2)).toBe(false) + expect(result.size).toBe(2) + }) + + it('should handle empty set', () => { + const set = new Set() + const result = flipSet(set, 1) + + expect(result.has(1)).toBe(true) + expect(result.size).toBe(1) + }) + + it('should handle string items', () => { + const set = new Set(['a', 'b', 'c']) + + flipSet(set, 'd') + expect(set.has('d')).toBe(true) + + flipSet(set, 'b') + expect(set.has('b')).toBe(false) + }) + + it('should handle object items', () => { + const obj1 = { id: 1 } + const obj2 = { id: 2 } + const set = new Set([obj1]) + + flipSet(set, obj2) + expect(set.has(obj2)).toBe(true) + expect(set.size).toBe(2) + + flipSet(set, obj1) + expect(set.has(obj1)).toBe(false) + expect(set.size).toBe(1) + }) + + it('should work with multiple flips', () => { + const set = new Set([1, 2]) + + flipSet(set, 3) // Add 3 + expect(set.size).toBe(3) + + flipSet(set, 3) // Remove 3 + expect(set.size).toBe(2) + + flipSet(set, 3) // Add 3 again + expect(set.size).toBe(3) + expect(set.has(3)).toBe(true) + }) + + it('should handle single element set', () => { + const set = new Set([42]) + + flipSet(set, 42) + expect(set.size).toBe(0) + + flipSet(set, 42) + expect(set.size).toBe(1) + }) + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/connection.ts b/foundations/core/packages/core/src/__tests__/connection.ts new file mode 100644 index 0000000000..93f2775f59 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/connection.ts @@ -0,0 +1,118 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { ClientConnectEvent, type DocChunk, generateId } from '..' +import type { Class, Doc, Domain, Ref, Timestamp } from '../classes' +import { type ClientConnection } from '../client' +import core from '../component' +import { Hierarchy } from '../hierarchy' +import { ModelDb, TxDb } from '../memdb' +import type { + DocumentQuery, + DomainResult, + FindResult, + SearchOptions, + SearchQuery, + SearchResult, + TxResult +} from '../storage' +import type { Tx } from '../tx' +import { DOMAIN_TX } from '../tx' +import { genMinModel } from './minmodel' + +export async function connect (handler: (tx: Tx) => void): Promise { + const txes = genMinModel() + + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + + const transactions = new TxDb(hierarchy) + const model = new ModelDb(hierarchy) + for (const tx of txes) { + await transactions.tx(tx) + await model.tx(tx) + } + + async function findAll (_class: Ref>, query: DocumentQuery): Promise> { + const domain = hierarchy.getClass(_class).domain + if (domain === DOMAIN_TX) return await transactions.findAll(_class, query) + return await model.findAll(_class, query) + } + + class ClientConnectionImpl implements ClientConnection { + isConnected = (): boolean => true + findAll = findAll + + handler?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise + + set onConnect ( + handler: ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined + ) { + this.handler = handler + void this.handler?.(ClientConnectEvent.Connected, '', {}) + } + + get onConnect (): ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined { + return this.handler + } + + async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { + return { docs: [] } + } + + pushHandler = (): void => {} + + async tx (tx: Tx): Promise { + if (tx.objectSpace === core.space.Model) { + hierarchy.tx(tx) + } + const result = await Promise.all([model.tx(tx), transactions.tx(tx)]) + return result[0] + } + + async domainRequest (): Promise { + return await Promise.resolve({ domain: 'test' as Domain, value: null }) + } + + async close (): Promise {} + + async loadChunk (domain: Domain, idx?: number): Promise { + return { + idx: -1, + docs: [], + finished: true + } + } + + async getDomainHash (domain: Domain): Promise { + return generateId() + } + + async closeChunk (idx: number): Promise {} + async loadDocs (domain: Domain, docs: Ref[]): Promise { + return [] + } + + async upload (domain: Domain, docs: Doc[]): Promise {} + async clean (domain: Domain, docs: Ref[]): Promise {} + async loadModel (last: Timestamp): Promise { + return txes + } + + async sendForceClose (): Promise {} + } + + return new ClientConnectionImpl() +} diff --git a/foundations/core/packages/core/src/__tests__/contexts.test.ts b/foundations/core/packages/core/src/__tests__/contexts.test.ts new file mode 100644 index 0000000000..ff93c75bfe --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/contexts.test.ts @@ -0,0 +1,22 @@ +import { MeasureMetricsContext } from '@hcengineering/measurements' + +describe('context tests', () => { + it('check withLog proper catch', async () => { + const ctx = new MeasureMetricsContext('test', {}) + + try { + await ctx.with( + 'failed op', + {}, + async () => { + throw new Error('failed') + }, + undefined, + { log: true } + ) + expect(true).toBe(false) + } catch (err: any) { + expect(err.message).toBe('failed') + } + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/hierarchy.test.ts b/foundations/core/packages/core/src/__tests__/hierarchy.test.ts new file mode 100644 index 0000000000..b6cdf4c507 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/hierarchy.test.ts @@ -0,0 +1,1121 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { AnyAttribute, Class, Doc, Obj, Ref } from '../classes' +import { ClassifierKind, DOMAIN_MODEL } from '../classes' +import type { TxCreateDoc } from '../tx' +import { TxFactory } from '../tx' +import core from '../component' +import { Hierarchy } from '../hierarchy' +import * as Proxy from '../proxy' +import { genMinModel, test } from './minmodel' + +const txes = genMinModel() + +function prepare (): Hierarchy { + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + return hierarchy +} + +describe('hierarchy', () => { + it('should build hierarchy', async () => { + const hierarchy = prepare() + const ancestors = hierarchy.getAncestors(core.class.TxCreateDoc) + expect(ancestors).toContain(core.class.Tx) + }) + + it('isDerived', async () => { + const hierarchy = prepare() + const derived = hierarchy.isDerived(core.class.Space, core.class.Doc) + expect(derived).toBeTruthy() + const notDerived = hierarchy.isDerived(core.class.Space, core.class.Class) + expect(notDerived).not.toBeTruthy() + }) + + it('isImplements', async () => { + const hierarchy = prepare() + let isImplements = hierarchy.isImplements(test.class.Task, test.interface.WithState) + expect(isImplements).toBeTruthy() + + isImplements = hierarchy.isImplements(test.class.TaskCheckItem, test.interface.WithState) + expect(isImplements).toBeTruthy() + + const notImplements = hierarchy.isImplements(core.class.Space, test.interface.WithState) + expect(notImplements).not.toBeTruthy() + }) + + it('getClass', async () => { + const hierarchy = prepare() + const data = hierarchy.getClass(core.class.TxCreateDoc) + expect(data).toMatchObject((txes.find((p) => p.objectId === core.class.TxCreateDoc) as TxCreateDoc).attributes) + const notExistClass = 'class:test.MyClass' as Ref> + expect(() => hierarchy.getClass(notExistClass)).toThrowError('class not found: ' + notExistClass) + }) + + it('getDomain', async () => { + const hierarchy = prepare() + const txDomain = hierarchy.getDomain(core.class.TxCreateDoc) + expect(txDomain).toBe('tx') + const modelDomain = hierarchy.getDomain(core.class.Class) + expect(modelDomain).toBe('model') + }) + + it('should create Mixin proxy', async () => { + const spyProxy = jest.spyOn(Proxy, '_createMixinProxy') + const hierarchy = prepare() + + hierarchy.as(txes[0], test.mixin.TestMixin) + expect(spyProxy).toBeCalledTimes(1) + + hierarchy.as(txes[0], test.mixin.TestMixin) + expect(spyProxy).toBeCalledTimes(1) + + spyProxy.mockReset() + spyProxy.mockRestore() + }) + + it('should call static methods', async () => { + const spyToDoc = jest.spyOn(Proxy, '_toDoc') + Hierarchy.toDoc(txes[0]) + expect(spyToDoc).toBeCalledTimes(1) + spyToDoc.mockReset() + spyToDoc.mockRestore() + + const spyMixinClass = jest.spyOn(Proxy, '_mixinClass') + Hierarchy.mixinClass(txes[0]) + expect(spyMixinClass).toBeCalledTimes(1) + + spyMixinClass.mockImplementationOnce(() => undefined).mockImplementationOnce(() => test.mixin.TestMixin) + let result = Hierarchy.mixinOrClass(txes[0]) + expect(result).toStrictEqual(txes[0]._class) + result = Hierarchy.mixinOrClass(txes[0]) + expect(result).toStrictEqual(test.mixin.TestMixin) + expect(spyMixinClass).toBeCalledTimes(3) + + spyMixinClass.mockReset() + spyMixinClass.mockRestore() + }) + + // Memory optimization tests - ancestors stored as array + it('getAncestors should return array directly', async () => { + const hierarchy = prepare() + const ancestors = hierarchy.getAncestors(core.class.TxCreateDoc) + + // Verify it's an array + expect(Array.isArray(ancestors)).toBeTruthy() + + // Verify it contains expected ancestors + expect(ancestors).toContain(core.class.TxCreateDoc) + expect(ancestors).toContain(core.class.TxCUD) + expect(ancestors).toContain(core.class.Tx) + expect(ancestors).toContain(core.class.Doc) + expect(ancestors).toContain(core.class.Obj) + + // Verify order is consistent + const indexTx = ancestors.indexOf(core.class.Tx) + const indexDoc = ancestors.indexOf(core.class.Doc) + expect(indexDoc).toBeGreaterThan(indexTx) + }) + + it('isDerived should work with array-based ancestors', async () => { + const hierarchy = prepare() + + // Test various inheritance chains + expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.Tx)).toBeTruthy() + expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.TxCUD)).toBeTruthy() + expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.Doc)).toBeTruthy() + expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.Obj)).toBeTruthy() + + // Test self-derivation (class is in its own ancestors) + expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.TxCreateDoc)).toBeTruthy() + + // Test non-derived classes + expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.Space)).toBeFalsy() + expect(hierarchy.isDerived(core.class.Space, core.class.Tx)).toBeFalsy() + + // Test with mixins + expect(hierarchy.isDerived(test.class.TestComment, core.class.AttachedDoc)).toBeTruthy() + expect(hierarchy.isDerived(test.mixin.TaskMixinTodos, test.class.Task)).toBeTruthy() + }) + + it('should handle deep inheritance chains efficiently', async () => { + const hierarchy = prepare() + + // TxCreateDoc has a chain: TxCreateDoc -> TxCUD -> Tx -> Doc -> Obj + const ancestors = hierarchy.getAncestors(core.class.TxCreateDoc) + expect(ancestors.length).toBeGreaterThanOrEqual(5) + + // All intermediate classes should be present + expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.TxCUD)).toBeTruthy() + expect(hierarchy.isDerived(core.class.TxCUD, core.class.Tx)).toBeTruthy() + expect(hierarchy.isDerived(core.class.Tx, core.class.Doc)).toBeTruthy() + expect(hierarchy.isDerived(core.class.Doc, core.class.Obj)).toBeTruthy() + }) + + // Classifier properties tests - Map-based storage + it('getClassifierProp and setClassifierProp should work with Map', async () => { + const hierarchy = prepare() + + // Set a property + hierarchy.setClassifierProp(core.class.Space, 'testProp', 'testValue') + + // Get the property + const value = hierarchy.getClassifierProp(core.class.Space, 'testProp') + expect(value).toBe('testValue') + + // Update the property + hierarchy.setClassifierProp(core.class.Space, 'testProp', 'updatedValue') + const updatedValue = hierarchy.getClassifierProp(core.class.Space, 'testProp') + expect(updatedValue).toBe('updatedValue') + }) + + it('should handle multiple properties per classifier', async () => { + const hierarchy = prepare() + + // Set multiple properties + hierarchy.setClassifierProp(core.class.Space, 'prop1', 'value1') + hierarchy.setClassifierProp(core.class.Space, 'prop2', 'value2') + hierarchy.setClassifierProp(core.class.Space, 'prop3', 42) + hierarchy.setClassifierProp(core.class.Space, 'prop4', { nested: 'object' }) + + // Verify all properties are stored correctly + expect(hierarchy.getClassifierProp(core.class.Space, 'prop1')).toBe('value1') + expect(hierarchy.getClassifierProp(core.class.Space, 'prop2')).toBe('value2') + expect(hierarchy.getClassifierProp(core.class.Space, 'prop3')).toBe(42) + expect(hierarchy.getClassifierProp(core.class.Space, 'prop4')).toEqual({ nested: 'object' }) + + // Verify undefined for non-existent property + expect(hierarchy.getClassifierProp(core.class.Space, 'nonExistent')).toBeUndefined() + }) + + it('should isolate properties between different classifiers', async () => { + const hierarchy = prepare() + + // Set properties on different classifiers + hierarchy.setClassifierProp(core.class.Space, 'name', 'Space') + hierarchy.setClassifierProp(core.class.Doc, 'name', 'Doc') + hierarchy.setClassifierProp(test.class.Task, 'name', 'Task') + + // Verify isolation + expect(hierarchy.getClassifierProp(core.class.Space, 'name')).toBe('Space') + expect(hierarchy.getClassifierProp(core.class.Doc, 'name')).toBe('Doc') + expect(hierarchy.getClassifierProp(test.class.Task, 'name')).toBe('Task') + }) + + it('should handle property updates without creating new objects', async () => { + const hierarchy = prepare() + + // Set initial value + hierarchy.setClassifierProp(core.class.Space, 'counter', 0) + + // Update multiple times (testing that we're not creating new objects each time) + for (let i = 1; i <= 100; i++) { + hierarchy.setClassifierProp(core.class.Space, 'counter', i) + } + + // Verify final value + expect(hierarchy.getClassifierProp(core.class.Space, 'counter')).toBe(100) + }) + + // Edge cases and integration tests + it('should handle interface implementation checks correctly', async () => { + const hierarchy = prepare() + + // Task implements DummyWithState which extends WithState + expect(hierarchy.isImplements(test.class.Task, test.interface.WithState)).toBeTruthy() + expect(hierarchy.isImplements(test.class.Task, test.interface.DummyWithState)).toBeTruthy() + + // TaskCheckItem directly implements WithState + expect(hierarchy.isImplements(test.class.TaskCheckItem, test.interface.WithState)).toBeTruthy() + + // Negative cases + expect(hierarchy.isImplements(core.class.Space, test.interface.WithState)).toBeFalsy() + }) + + it('should maintain consistency after multiple hierarchy operations', async () => { + const hierarchy = prepare() + + // Perform multiple operations + const ancestors1 = hierarchy.getAncestors(test.class.Task) + const isDerived1 = hierarchy.isDerived(test.class.Task, core.class.Doc) + + // Set some properties + hierarchy.setClassifierProp(test.class.Task, 'test', 'value') + + // Verify operations still work correctly + const ancestors2 = hierarchy.getAncestors(test.class.Task) + const isDerived2 = hierarchy.isDerived(test.class.Task, core.class.Doc) + + expect(ancestors1).toEqual(ancestors2) + expect(isDerived1).toBe(isDerived2) + expect(isDerived2).toBeTruthy() + }) + + it('should handle getDescendants correctly', async () => { + const hierarchy = prepare() + + // Get descendants of Doc (should include many classes) + const descendants = hierarchy.getDescendants(core.class.Doc) + + expect(descendants).toContain(core.class.Space) + expect(descendants).toContain(core.class.Tx) + expect(descendants).toContain(test.class.Task) + expect(Array.isArray(descendants)).toBeTruthy() + }) + + it('should work with getBaseClass', async () => { + const hierarchy = prepare() + + // Get base class of a mixin + const baseClass = hierarchy.getBaseClass(test.mixin.TaskMixinTodos) + expect(baseClass).toBe(test.class.Task) + + // Get base class of a regular class (should return itself) + const baseClass2 = hierarchy.getBaseClass(test.class.Task) + expect(baseClass2).toBe(test.class.Task) + }) + + it('should handle getAllAttributes correctly', async () => { + const hierarchy = prepare() + + // Get all attributes for a class + const attributes = hierarchy.getAllAttributes(core.class.TxCreateDoc) + + // Should return a Map + expect(attributes instanceof Map).toBeTruthy() + + // Test with to parameter + const attributesTo = hierarchy.getAllAttributes(core.class.TxCreateDoc, core.class.Tx) + expect(attributesTo instanceof Map).toBeTruthy() + }) + + it('should maintain immutability of returned ancestors array', async () => { + const hierarchy = prepare() + + // Get ancestors + const ancestors = hierarchy.getAncestors(test.class.Task) + const originalLength = ancestors.length + + // The returned array should be the internal array, but modifying it shouldn't break hierarchy + // (This is a trade-off for memory optimization - callers should treat it as read-only) + expect(ancestors.length).toBe(originalLength) + expect(ancestors).toContain(core.class.Doc) + }) + + it('should handle performance for multiple isDerived checks', async () => { + const hierarchy = prepare() + + // Perform many isDerived checks to ensure array-based lookup is performant + const startTime = Date.now() + for (let i = 0; i < 1000; i++) { + hierarchy.isDerived(core.class.TxCreateDoc, core.class.Tx) + hierarchy.isDerived(test.class.Task, core.class.Doc) + hierarchy.isDerived(test.class.TaskCheckItem, core.class.AttachedDoc) + } + const endTime = Date.now() + + // Should complete in reasonable time (< 100ms for 3000 checks) + expect(endTime - startTime).toBeLessThan(100) + }) + + // Additional comprehensive tests for better coverage + + it('should handle findClass and hasClass correctly', async () => { + const hierarchy = prepare() + + // findClass should return class or undefined + const foundClass = hierarchy.findClass(core.class.Space) + expect(foundClass).toBeDefined() + expect(foundClass?._id).toBe(core.class.Space) + + const notFoundClass = hierarchy.findClass('class:NonExistent' as Ref>) + expect(notFoundClass).toBeUndefined() + + // hasClass should return boolean + expect(hierarchy.hasClass(core.class.Space)).toBeTruthy() + expect(hierarchy.hasClass('class:NonExistent' as Ref>)).toBeFalsy() + + // Interface should not be considered a class + expect(hierarchy.hasClass(test.interface.WithState as any)).toBeFalsy() + }) + + it('should handle getClassOrInterface correctly', async () => { + const hierarchy = prepare() + + // Should get a class + const spaceClass = hierarchy.getClassOrInterface(core.class.Space) + expect(spaceClass._id).toBe(core.class.Space) + + // Should get an interface + const withStateInterface = hierarchy.getClassOrInterface(test.interface.WithState as any) + expect(withStateInterface._id).toBe(test.interface.WithState) + + // Should throw for non-existent + expect(() => hierarchy.getClassOrInterface('class:NonExistent' as Ref>)).toThrowError( + 'class not found: class:NonExistent' + ) + }) + + it('should handle getInterface correctly', async () => { + const hierarchy = prepare() + + // Should get interface + const withStateInterface = hierarchy.getInterface(test.interface.WithState) + expect(withStateInterface._id).toBe(test.interface.WithState) + + // Should throw for non-existent interface + expect(() => hierarchy.getInterface('interface:NonExistent' as any)).toThrowError( + 'interface not found: interface:NonExistent' + ) + + // Should throw for class (not interface) + expect(() => hierarchy.getInterface(core.class.Space as any)).toThrowError() + }) + + it('should handle isMixin correctly', async () => { + const hierarchy = prepare() + + // Should identify mixins + expect(hierarchy.isMixin(test.mixin.TestMixin)).toBeTruthy() + expect(hierarchy.isMixin(test.mixin.TaskMixinTodos)).toBeTruthy() + expect(hierarchy.isMixin(core.class.AttachedDoc)).toBeTruthy() + + // Should not identify classes as mixins + expect(hierarchy.isMixin(core.class.Space)).toBeFalsy() + expect(hierarchy.isMixin(test.class.Task)).toBeFalsy() + + // Should return false for non-existent + expect(hierarchy.isMixin('mixin:NonExistent' as any)).toBeFalsy() + }) + + it('should handle as and asIf with mixins', async () => { + const hierarchy = prepare() + + const doc = { + _id: 'doc1' as any, + _class: core.class.Doc, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any + } + + // as should return a proxy + const withMixin = hierarchy.as(doc, test.mixin.TestMixin) + expect(withMixin).toBeDefined() + expect(withMixin._id).toBe(doc._id) + + // asIf should return undefined if mixin not present + const asIfResult = hierarchy.asIf(doc, test.mixin.TestMixin) + expect(asIfResult).toBeUndefined() + + // asIf should return undefined for undefined doc + expect(hierarchy.asIf(undefined, test.mixin.TestMixin)).toBeUndefined() + }) + + it('should handle asIfArray correctly', async () => { + const hierarchy = prepare() + + const docs = [ + { _id: 'doc1' as any, _class: core.class.Doc, space: 'space1' as any, modifiedOn: 0, modifiedBy: 'user1' as any }, + { _id: 'doc2' as any, _class: core.class.Doc, space: 'space1' as any, modifiedOn: 0, modifiedBy: 'user1' as any } + ] + + // Should return empty array if no docs have the mixin + const result = hierarchy.asIfArray(docs, test.mixin.TestMixin) + expect(Array.isArray(result)).toBeTruthy() + expect(result.length).toBe(0) + }) + + it('should handle classHierarchyMixin correctly', async () => { + const hierarchy = prepare() + + // Should find mixin in class hierarchy + const result = hierarchy.classHierarchyMixin(test.mixin.TaskMixinTodos, test.mixin.TestMixin) + // May be undefined if mixin not in hierarchy + expect(result === undefined || typeof result === 'object').toBeTruthy() + + // Test with filter + const filtered = hierarchy.classHierarchyMixin(test.class.Task, test.mixin.TestMixin, (m) => m !== undefined) + expect(filtered === undefined || typeof filtered === 'object').toBeTruthy() + }) + + it('should handle findClassOrMixinMixin correctly', async () => { + const hierarchy = prepare() + + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any + } + + const result = hierarchy.findClassOrMixinMixin(doc, test.mixin.TestMixin) + // May be undefined if not found + expect(result === undefined || typeof result === 'object').toBeTruthy() + }) + + it('should handle findMixinMixins correctly', async () => { + const hierarchy = prepare() + + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any + } + + const results = hierarchy.findMixinMixins(doc, test.mixin.TestMixin) + expect(Array.isArray(results)).toBeTruthy() + }) + + it('should handle findAllMixins correctly', async () => { + const hierarchy = prepare() + + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any + } + + const mixins = hierarchy.findAllMixins(doc) + expect(Array.isArray(mixins)).toBeTruthy() + }) + + it('should handle findDomain correctly', async () => { + const hierarchy = prepare() + + // Should find domain + const domain = hierarchy.findDomain(core.class.Space) + expect(domain).toBe('model') + + // Should return undefined for non-existent class + const noDomain = hierarchy.findDomain('class:NonExistent' as any) + expect(noDomain).toBeUndefined() + }) + + it('should handle getParentClass correctly', async () => { + const hierarchy = prepare() + + // Should get parent class with same domain + const parent = hierarchy.getParentClass(core.class.Space) + expect(parent).toBeDefined() + // Parent should have same domain or be the class itself + const parentDomain = hierarchy.findDomain(parent) + expect(parentDomain === 'model' || parent === core.class.Space).toBeTruthy() + }) + + it('should handle getAttribute correctly', async () => { + const hierarchy = prepare() + + // Should throw for non-existent attribute + expect(() => hierarchy.getAttribute(core.class.Space, 'nonExistentAttr')).toThrowError( + 'attribute not found: nonExistentAttr' + ) + }) + + it('should handle findAttribute correctly', async () => { + const hierarchy = prepare() + + // Should return undefined for non-existent attribute + const attr = hierarchy.findAttribute(core.class.Space, 'nonExistentAttr') + expect(attr).toBeUndefined() + }) + + it('should handle getOwnAttributes correctly', async () => { + const hierarchy = prepare() + + // Should return Map of attributes + const attrs = hierarchy.getOwnAttributes(core.class.Space) + expect(attrs instanceof Map).toBeTruthy() + }) + + it('should handle updateLookupMixin correctly', async () => { + const hierarchy = prepare() + + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any + } + + // Without $lookup + const result1 = hierarchy.updateLookupMixin(test.class.Task, doc as any) + expect(result1).toBeDefined() + + // With $lookup + const docWithLookup = { + ...doc, + $lookup: {} + } + const result2 = hierarchy.updateLookupMixin(test.class.Task, docWithLookup as any) + expect(result2).toBeDefined() + }) + + it('should handle clone correctly', async () => { + const hierarchy = prepare() + + const obj = { + _id: 'doc1' as any, + _class: core.class.Doc, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any, + nested: { + value: 'test' + } + } + + const cloned = hierarchy.clone(obj) + expect(cloned).toEqual(obj) + expect(cloned).not.toBe(obj) + expect(cloned.nested).not.toBe(obj.nested) + }) + + it('should handle domains correctly', async () => { + const hierarchy = prepare() + + const domains = hierarchy.domains() + expect(Array.isArray(domains)).toBeTruthy() + expect(domains).toContain('model') + expect(domains).toContain('tx') + + // Should not have duplicates + const uniqueDomains = [...new Set(domains)] + expect(domains.length).toBe(uniqueDomains.length) + }) + + it('should handle getAncestors error case', async () => { + const hierarchy = prepare() + + // Should throw for non-existent class + expect(() => hierarchy.getAncestors('class:NonExistent' as any)).toThrowError( + 'ancestors not found: class:NonExistent' + ) + }) + + it('should handle getDescendants error case', async () => { + const hierarchy = prepare() + + // Should throw for non-existent class + expect(() => hierarchy.getDescendants('class:NonExistent' as any)).toThrowError( + 'descendants not found: class:NonExistent' + ) + }) + + it('should handle getDomain error case', async () => { + const hierarchy = prepare() + + // Should throw for class without domain + expect(() => hierarchy.getDomain('class:NonExistent' as any)).toThrowError('domain not found: class:NonExistent') + }) + + it('should handle static hasMixin correctly', async () => { + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any, + [test.mixin.TestMixin]: { arr: [] } + } + + expect(Hierarchy.hasMixin(doc, test.mixin.TestMixin)).toBeTruthy() + expect(Hierarchy.hasMixin(doc, 'mixin:Other' as any)).toBeFalsy() + }) + + it('should maintain descendants list correctly', async () => { + const hierarchy = prepare() + + // Doc should have many descendants + const docDescendants = hierarchy.getDescendants(core.class.Doc) + expect(docDescendants.length).toBeGreaterThan(5) + + // Obj should have even more descendants (everything) + const objDescendants = hierarchy.getDescendants(core.class.Obj) + expect(objDescendants.length).toBeGreaterThanOrEqual(docDescendants.length) + + // TxCreateDoc should have fewer descendants (possibly none) + const txCreateDescendants = hierarchy.getDescendants(core.class.TxCreateDoc) + expect(Array.isArray(txCreateDescendants)).toBeTruthy() + }) + + it('should handle interface extends chains', async () => { + const hierarchy = prepare() + + // DummyWithState extends WithState + const dummyAncestors = hierarchy.getAncestors(test.interface.DummyWithState) + expect(dummyAncestors).toContain(test.interface.WithState) + }) + + it('should handle complex mixin hierarchies', async () => { + const hierarchy = prepare() + + // TaskMixinTodos extends Task + expect(hierarchy.isDerived(test.mixin.TaskMixinTodos, test.class.Task)).toBeTruthy() + expect(hierarchy.isDerived(test.mixin.TaskMixinTodos, core.class.Doc)).toBeTruthy() + + // Should identify as mixin + expect(hierarchy.isMixin(test.mixin.TaskMixinTodos)).toBeTruthy() + + // Base class should be Task + const baseClass = hierarchy.getBaseClass(test.mixin.TaskMixinTodos) + expect(baseClass).toBe(test.class.Task) + }) + + it('should handle getAllAttributes with traverse callback', async () => { + const hierarchy = prepare() + + const traversed: Array<{ name: string, attrId: string }> = [] + const attributes = hierarchy.getAllAttributes(core.class.TxCreateDoc, undefined, (name, attr) => { + traversed.push({ name, attrId: attr._id }) + }) + + expect(attributes instanceof Map).toBeTruthy() + // If there are attributes, traverse should have been called + if (attributes.size > 0) { + expect(traversed.length).toBeGreaterThan(0) + } + }) + + it('should handle property deletions implicitly through cache invalidation', async () => { + const hierarchy = prepare() + + // Set a property + hierarchy.setClassifierProp(core.class.Space, 'cached', 'value') + expect(hierarchy.getClassifierProp(core.class.Space, 'cached')).toBe('value') + + // Properties persist across reads + expect(hierarchy.getClassifierProp(core.class.Space, 'cached')).toBe('value') + }) + + it('should verify memory optimization: no duplicate ancestor storage', async () => { + const hierarchy = prepare() + + // Get ancestors for multiple classes + const ancestors1 = hierarchy.getAncestors(core.class.TxCreateDoc) + const ancestors2 = hierarchy.getAncestors(core.class.TxUpdateDoc) + const ancestors3 = hierarchy.getAncestors(core.class.TxRemoveDoc) + + // All should share some common ancestors + const commonAncestors = ancestors1.filter((a) => ancestors2.includes(a) && ancestors3.includes(a)) + + expect(commonAncestors).toContain(core.class.TxCUD) + expect(commonAncestors).toContain(core.class.Tx) + expect(commonAncestors).toContain(core.class.Doc) + + // Verify arrays are distinct objects (not shared references) + expect(ancestors1).not.toBe(ancestors2) + expect(ancestors2).not.toBe(ancestors3) + }) + + // Additional tests for higher coverage + + it('should handle classHierarchyMixin with filter that returns false', async () => { + const hierarchy = prepare() + + // Create a class that has TestMixin + const clazz = hierarchy.getClass(core.class.Doc) + // Apply mixin to class + const withMixin = hierarchy.as(clazz, test.mixin.TestMixin) + expect(withMixin).toBeDefined() + + // Test with filter that returns false + const result = hierarchy.classHierarchyMixin( + core.class.Doc, + test.mixin.TestMixin, + (m) => false // Filter that always returns false + ) + expect(result).toBeUndefined() + }) + + it('should handle findClassOrMixinMixin with document having mixins', async () => { + const hierarchy = prepare() + + // Create a document with a mixin property + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any, + [test.mixin.TestMixin]: { arr: ['item1'] } + } + + const result = hierarchy.findClassOrMixinMixin(doc, test.mixin.TestMixin) + expect(result === undefined || typeof result === 'object').toBeTruthy() + }) + + it('should handle findMixinMixins with document having multiple mixins', async () => { + const hierarchy = prepare() + + // Create classes with mixins + const taskClass = hierarchy.getClass(test.class.Task) + // Test that class can have mixin applied + hierarchy.as(taskClass, test.mixin.TestMixin) + + // Create document with mixin applied + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any, + [test.mixin.TestMixin]: { arr: ['item1'] } + } + + const results = hierarchy.findMixinMixins(doc, test.mixin.TestMixin) + expect(Array.isArray(results)).toBeTruthy() + }) + + it('should handle findAllMixins with document having mixins', async () => { + const hierarchy = prepare() + + // Create document with mixins + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any, + [test.mixin.TestMixin]: { arr: ['item1'] } + } + + const mixins = hierarchy.findAllMixins(doc) + expect(Array.isArray(mixins)).toBeTruthy() + if (mixins.length > 0) { + expect(mixins).toContain(test.mixin.TestMixin) + } + }) + + it('should handle tx with TxMixin', async () => { + const hierarchy = new Hierarchy() + // Build basic hierarchy + for (const tx of txes) { + hierarchy.tx(tx) + } + + // Create a TxMixin transaction + const txFactory = new TxFactory(core.account.System) + const mixinTx = txFactory.createTxMixin( + core.class.Space as any, + core.class.Class as any, + core.space.Model, + test.mixin.TestMixin, + { arr: ['test'] } + ) + + // Apply the mixin transaction + hierarchy.tx(mixinTx) + + // Verify the mixin was applied + const spaceClass = hierarchy.getClass(core.class.Space) + expect(spaceClass).toBeDefined() + }) + + it('should handle txUpdateDoc with Attribute', async () => { + const hierarchy = new Hierarchy() + // Build basic hierarchy + for (const tx of txes) { + hierarchy.tx(tx) + } + + // Create an attribute first + const txFactory = new TxFactory(core.account.System) + const attrId = 'attr:test' as Ref + + const createAttrTx = txFactory.createTxCreateDoc( + core.class.Attribute, + core.space.Model, + { + attributeOf: core.class.Space, + name: 'testAttr', + type: { _class: 'class:core.Type' as any } + }, + attrId + ) + + hierarchy.tx(createAttrTx) + + // Now update the attribute + const updateAttrTx = txFactory.createTxUpdateDoc(core.class.Attribute, core.space.Model, attrId, { + name: 'updatedAttr' + }) + + hierarchy.tx(updateAttrTx) + + // Verify the attribute was updated + const attr = hierarchy.findAttribute(core.class.Space, 'updatedAttr') + expect(attr).toBeDefined() + }) + + it('should handle txRemoveDoc with Attribute', async () => { + const hierarchy = new Hierarchy() + // Build basic hierarchy + for (const tx of txes) { + hierarchy.tx(tx) + } + + // Create an attribute first + const txFactory = new TxFactory(core.account.System) + const attrId = 'attr:test' as Ref + + const createAttrTx = txFactory.createTxCreateDoc( + core.class.Attribute, + core.space.Model, + { + attributeOf: core.class.Space, + name: 'testAttr', + type: { _class: 'class:core.Type' as any } + }, + attrId + ) + + hierarchy.tx(createAttrTx) + + // Verify attribute exists + let attr = hierarchy.findAttribute(core.class.Space, 'testAttr') + expect(attr).toBeDefined() + + // Now remove the attribute + const removeAttrTx = txFactory.createTxRemoveDoc(core.class.Attribute, core.space.Model, attrId) + + hierarchy.tx(removeAttrTx) + + // Verify the attribute was removed + attr = hierarchy.findAttribute(core.class.Space, 'testAttr') + expect(attr).toBeUndefined() + }) + + it('should handle txUpdateDoc with Classifier', async () => { + const hierarchy = new Hierarchy() + // Build basic hierarchy + for (const tx of txes) { + hierarchy.tx(tx) + } + + const txFactory = new TxFactory(core.account.System) + + // Update a classifier + const updateClassTx = txFactory.createTxUpdateDoc(core.class.Class, core.space.Model, core.class.Space, { + label: 'Updated Space' as any + }) + + hierarchy.tx(updateClassTx) + + const spaceClass = hierarchy.getClass(core.class.Space) + expect(spaceClass.label).toBe('Updated Space' as any) + }) + + it('should handle txRemoveDoc with Classifier', async () => { + const hierarchy = new Hierarchy() + // Build basic hierarchy + for (const tx of txes) { + hierarchy.tx(tx) + } + + const txFactory = new TxFactory(core.account.System) + + // Create a new class + const newClassId = 'class:test.NewClass' as Ref> + const createClassTx = txFactory.createTxCreateDoc( + core.class.Class, + core.space.Model, + { + label: 'NewClass' as any, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }, + newClassId + ) + + hierarchy.tx(createClassTx) + + // Verify class exists + expect(hierarchy.hasClass(newClassId)).toBeTruthy() + + // Remove the class + const removeClassTx = txFactory.createTxRemoveDoc(core.class.Class, core.space.Model, newClassId) + + hierarchy.tx(removeClassTx) + + // Verify class was removed + expect(hierarchy.hasClass(newClassId)).toBeFalsy() + }) + + it('should handle updateLookupMixin with lookup containing mixins', async () => { + const hierarchy = prepare() + + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any, + $lookup: { + space: { + _id: 'space1' as any, + _class: core.class.Space, + name: 'Test Space', + description: '', + private: false, + members: [], + archived: false, + modifiedOn: 0, + modifiedBy: 'user1' as any + } + } + } + + const options = { + lookup: { + space: core.class.Space + } + } + + const result = hierarchy.updateLookupMixin(test.class.Task, doc as any, options as any) + expect(result).toBeDefined() + expect(result.$lookup).toBeDefined() + }) + + it('should handle updateLookupMixin with _id lookup containing mixins', async () => { + const hierarchy = prepare() + + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any, + $lookup: { + _id: { + _class: core.class.Space, + _id: 'space1' as any, + name: 'Test', + description: '', + private: false, + members: [], + archived: false, + modifiedOn: 0, + modifiedBy: 'user1' as any + } + } + } + + const options = { + lookup: { + _id: { + _class: test.mixin.TestMixin + } + } + } + + const result = hierarchy.updateLookupMixin(test.class.Task, doc as any, options as any) + expect(result).toBeDefined() + }) + + it('should handle updateLookupMixin with array in _id lookup', async () => { + const hierarchy = prepare() + + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any, + $lookup: { + _id: [ + { + _class: core.class.Space, + _id: 'space1' as any, + name: 'Test', + description: '', + private: false, + members: [], + archived: false, + modifiedOn: 0, + modifiedBy: 'user1' as any + } + ] + } + } + + const options = { + lookup: { + _id: { + _class: [test.mixin.TestMixin] + } + } + } + + const result = hierarchy.updateLookupMixin(test.class.Task, doc as any, options as any) + expect(result).toBeDefined() + }) + + it('should handle updateLookupMixin with null lookup value', async () => { + const hierarchy = prepare() + + const doc = { + _id: 'doc1' as any, + _class: test.class.Task, + space: 'space1' as any, + modifiedOn: 0, + modifiedBy: 'user1' as any, + $lookup: { + space: null + } + } + + const options = { + lookup: { + space: test.mixin.TestMixin + } + } + + const result = hierarchy.updateLookupMixin(test.class.Task, doc as any, options as any) + expect(result).toBeDefined() + expect(result.$lookup?.space).toBeNull() + }) + + it('should handle tx with non-matching class', async () => { + const hierarchy = new Hierarchy() + // Build basic hierarchy + for (const tx of txes) { + hierarchy.tx(tx) + } + + const txFactory = new TxFactory(core.account.System) + + // Create a transaction for a non-classifier class (Space) + const spaceTx = txFactory.createTxCreateDoc(core.class.Space, core.space.Model, { + name: 'Test Space', + description: '', + private: false, + members: [], + archived: false + }) + + // This should not throw, just handle gracefully + hierarchy.tx(spaceTx) + expect(true).toBeTruthy() + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/lang.test.ts b/foundations/core/packages/core/src/__tests__/lang.test.ts new file mode 100644 index 0000000000..cf96c6dcf7 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/lang.test.ts @@ -0,0 +1,6 @@ +import { makeLocalesTest } from '@hcengineering/platform' + +it( + 'Locales are equale', + makeLocalesTest((lang) => import(`../../lang/${lang}.json`)) +) diff --git a/foundations/core/packages/core/src/__tests__/limiter-edge-cases.test.ts b/foundations/core/packages/core/src/__tests__/limiter-edge-cases.test.ts new file mode 100644 index 0000000000..395e3fbfe8 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/limiter-edge-cases.test.ts @@ -0,0 +1,380 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { RateLimiter, TimeRateLimiter } from '../utils' + +describe('RateLimiter and TimeRateLimiter - Advanced Edge Cases', () => { + describe('RateLimiter - Memory and Resource Management', () => { + it('should not leak memory in notify array', async () => { + const limiter = new RateLimiter(1) + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return 'result' + }) + + // Execute multiple operations that will queue + const operations = Array(10) + .fill(0) + .map(() => limiter.exec(mockFn)) + + await Promise.all(operations) + + // notify array should be empty after all operations complete + expect(limiter.notify.length).toBe(0) + }) + + it('should clean up processingQueue correctly after many operations', async () => { + const limiter = new RateLimiter(5) + const mockFn = jest.fn().mockImplementation(async (args?: any) => { + await new Promise((resolve) => setTimeout(resolve, Math.random() * 10)) + return args?.id + }) + + for (let batch = 0; batch < 5; batch++) { + const operations = Array(20) + .fill(0) + .map((_, i) => limiter.exec(mockFn, { id: batch * 20 + i })) + + await Promise.all(operations) + expect(limiter.processingQueue.size).toBe(0) + } + + expect(mockFn).toHaveBeenCalledTimes(100) + }) + + it('should handle interleaved exec and add operations', async () => { + const limiter = new RateLimiter(2) + const results: string[] = [] + + const execOp = async (id: string): Promise => { + await new Promise((resolve) => setTimeout(resolve, 10)) + results.push(id) + return id + } + + const addOp = async (id: string): Promise => { + await new Promise((resolve) => setTimeout(resolve, 10)) + results.push(id) + } + + await Promise.all([ + limiter.exec(async () => await execOp('exec1')), + limiter.add(async () => { + await addOp('add1') + }), + limiter.exec(async () => await execOp('exec2')), + limiter.add(async () => { + await addOp('add2') + }) + ]) + + await limiter.waitProcessing() + + expect(results).toHaveLength(4) + expect(results).toContain('exec1') + expect(results).toContain('exec2') + expect(results).toContain('add1') + expect(results).toContain('add2') + }) + + it('should handle rapid creation and destruction of operations', async () => { + const limiter = new RateLimiter(3) + let successCount = 0 + let errorCount = 0 + + const operations = Array(30) + .fill(0) + .map(async (_, i) => { + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 5)) + if (i % 5 === 0) { + throw new Error(`Error ${i}`) + } + return `Result ${i}` + }) + + try { + await limiter.exec(mockFn) + successCount++ + } catch (err) { + errorCount++ + } + }) + + await Promise.all(operations) + + expect(successCount).toBe(24) + expect(errorCount).toBe(6) + expect(limiter.processingQueue.size).toBe(0) + }) + }) + + describe('TimeRateLimiter - Memory and Resource Management', () => { + it('should not leak memory in notify array', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(2, 1000) + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return 'result' + }) + + // Execute operations that will need to wait + const operations = [limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn)] + + jest.advanceTimersByTime(20) + await Promise.resolve() + await Promise.resolve() + + jest.advanceTimersByTime(1001) + await Promise.resolve() + await Promise.resolve() + + jest.advanceTimersByTime(20) + await Promise.resolve() + await Promise.resolve() + + await Promise.all(operations) + + // notify array should be empty after all operations complete + expect(limiter.notify.length).toBe(0) + jest.useRealTimers() + }, 10000) + + it('should not accumulate executions indefinitely', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(5, 1000) + const mockFn = jest.fn().mockResolvedValue('result') + + // Execute many batches + for (let batch = 0; batch < 10; batch++) { + const operations = Array(5) + .fill(0) + .map(() => limiter.exec(mockFn)) + + await Promise.all(operations) + + jest.advanceTimersByTime(1100) + } + + // Executions should be cleaned up + // Only the most recent batch should remain (or less) + expect(limiter.executions.length).toBeLessThanOrEqual(5) + jest.useRealTimers() + }) + + it('should handle operations that never resolve gracefully', async () => { + const limiter = new TimeRateLimiter(2, 1000) + + const normalOp = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return 'result' + }) + + // Note: We can't actually test hanging operations without causing issues + // Instead, test that slow operations don't block faster ones + const slowOp = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + return 'slow' + }) + + void limiter.exec(slowOp) + expect(limiter.active).toBe(1) + + // Other operations should still work + const result = await limiter.exec(normalOp) + expect(result).toBe('result') + expect(normalOp).toHaveBeenCalledTimes(1) + }, 10000) + }) + + describe('RateLimiter - Boundary Conditions', () => { + it('should handle operations that complete immediately', async () => { + const limiter = new RateLimiter(3) + const mockFn = jest.fn().mockResolvedValue('instant') + + const results = await Promise.all([limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn)]) + + expect(results).toEqual(['instant', 'instant', 'instant']) + expect(limiter.processingQueue.size).toBe(0) + }) + + it('should maintain correct state when operations fail at different stages', async () => { + const limiter = new RateLimiter(2) + const errors: string[] = [] + + const operations = [ + limiter + .exec(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + throw new Error('error1') + }) + .catch((e) => { + errors.push(e.message) + }), + + limiter.exec(async () => { + await new Promise((resolve) => setTimeout(resolve, 5)) + return 'success1' + }), + + limiter + .exec(async () => { + throw new Error('error2') + }) + .catch((e) => { + errors.push(e.message) + }), + + limiter.exec(async () => { + await new Promise((resolve) => setTimeout(resolve, 15)) + return 'success2' + }) + ] + + const results = await Promise.all(operations) + + expect(errors).toHaveLength(2) + expect(errors).toContain('error1') + expect(errors).toContain('error2') + expect(results.filter(Boolean)).toContain('success1') + expect(results.filter(Boolean)).toContain('success2') + expect(limiter.processingQueue.size).toBe(0) + }, 10000) + }) + + describe('TimeRateLimiter - Boundary Conditions', () => { + it('should handle operations completing in reverse order', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(3, 1000) + const completionOrder: number[] = [] + + const createOp = (id: number, delay: number) => async (): Promise => { + await new Promise((resolve) => setTimeout(resolve, delay)) + completionOrder.push(id) + return id + } + + const operations = [limiter.exec(createOp(1, 30)), limiter.exec(createOp(2, 20)), limiter.exec(createOp(3, 10))] + + jest.advanceTimersByTime(11) + await Promise.resolve() + + jest.advanceTimersByTime(10) + await Promise.resolve() + + jest.advanceTimersByTime(10) + await Promise.resolve() + + await Promise.all(operations) + + // Operations should complete in reverse order of their delays + expect(completionOrder).toEqual([3, 2, 1]) + jest.useRealTimers() + }) + + it('should handle rate limit at exact boundaries', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(2, 1000) + const mockFn = jest.fn().mockResolvedValue('result') + + // Execute exactly at the rate + void limiter.exec(mockFn) + void limiter.exec(mockFn) + + expect(mockFn).toHaveBeenCalledTimes(2) + expect(limiter.active).toBe(2) + + // One more should wait + const thirdOp = limiter.exec(mockFn) + expect(mockFn).toHaveBeenCalledTimes(2) + + // Advance to exactly the period + jest.advanceTimersByTime(1000) + await Promise.resolve() + await Promise.resolve() + + await thirdOp + expect(mockFn).toHaveBeenCalledTimes(3) + jest.useRealTimers() + }) + }) + + describe('Integration Scenarios', () => { + it('should handle mixed RateLimiter operations with varying complexities', async () => { + const limiter = new RateLimiter(3) + const results: Array = [] + + const stringOp = async (val: string): Promise => { + await new Promise((resolve) => setTimeout(resolve, 5)) + results.push(val) + return val + } + + const numberOp = async (val: number): Promise => { + await new Promise((resolve) => setTimeout(resolve, 10)) + results.push(val) + return val + } + + const objectOp = async (obj: { id: number }): Promise<{ id: number }> => { + await new Promise((resolve) => setTimeout(resolve, 7)) + results.push(obj.id) + return obj + } + + await Promise.all([ + limiter.exec(async () => await stringOp('a')), + limiter.exec(async () => await numberOp(1)), + limiter.exec(async () => await objectOp({ id: 100 })), + limiter.exec(async () => await stringOp('b')), + limiter.exec(async () => await numberOp(2)) + ]) + + expect(results).toHaveLength(5) + expect(results).toContain('a') + expect(results).toContain('b') + expect(results).toContain(1) + expect(results).toContain(2) + expect(results).toContain(100) + }) + + it('should handle nested rate limiters', async () => { + const outerLimiter = new RateLimiter(2) + const innerLimiter = new RateLimiter(1) + + let executionCount = 0 + + const nestedOp = async (id: number): Promise => { + return await innerLimiter.exec(async () => { + executionCount++ + await new Promise((resolve) => setTimeout(resolve, 5)) + return id + }) + } + + const results = await Promise.all([ + outerLimiter.exec(async () => await nestedOp(1)), + outerLimiter.exec(async () => await nestedOp(2)), + outerLimiter.exec(async () => await nestedOp(3)), + outerLimiter.exec(async () => await nestedOp(4)) + ]) + + expect(results).toEqual([1, 2, 3, 4]) + expect(executionCount).toBe(4) + expect(outerLimiter.processingQueue.size).toBe(0) + expect(innerLimiter.processingQueue.size).toBe(0) + }) + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/limits.test.ts b/foundations/core/packages/core/src/__tests__/limits.test.ts new file mode 100644 index 0000000000..2bfc263c15 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/limits.test.ts @@ -0,0 +1,462 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { TimeRateLimiter } from '../utils' + +describe('TimeRateLimiter', () => { + describe('constructor', () => { + it('should initialize with correct rate and period', () => { + const limiter = new TimeRateLimiter(5, 2000) + expect(limiter.rate).toBe(5) + expect(limiter.period).toBe(2000) + expect(limiter.active).toBe(0) + expect(limiter.executions).toEqual([]) + }) + + it('should use default period of 1000ms', () => { + const limiter = new TimeRateLimiter(3) + expect(limiter.period).toBe(1000) + }) + }) + + it('should limit rate of executions', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(2, 1000) // 2 executions per second + const mockFn = jest.fn().mockResolvedValue('result') + const operations: Promise[] = [] + + // Try to execute 4 operations + for (let i = 0; i < 4; i++) { + operations.push(limiter.exec(mockFn)) + } + + // First 2 should execute immediately + expect(mockFn).toHaveBeenCalledTimes(2) + + // Advance time by 1 second + jest.advanceTimersByTime(1001) + await Promise.resolve() + + // Next 2 should execute after time advance + expect(mockFn).toHaveBeenCalledTimes(4) + + await Promise.all(operations) + jest.useRealTimers() + }) + + it('should cleanup old executions', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(2, 1000) + const mockFn = jest.fn().mockResolvedValue('result') + + // Execute first operation + await limiter.exec(mockFn) + expect(mockFn).toHaveBeenCalledTimes(1) + expect(limiter.executions.length).toBe(1) + + // Advance time past period + jest.advanceTimersByTime(1001) + + // Execute another operation + await limiter.exec(mockFn) + expect(mockFn).toHaveBeenCalledTimes(2) + expect(limiter.executions.length).toBe(1) // Old execution should be cleaned up + jest.useRealTimers() + }) + + it('should handle concurrent operations', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(2, 1000) + const mockFn = jest.fn().mockImplementation(async () => { + console.log('start#') + await new Promise((resolve) => setTimeout(resolve, 450)) + console.log('finished#') + return 'result' + }) + + const operations = Promise.all([limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn)]) + + expect(mockFn).toHaveBeenCalledTimes(2) + expect(limiter.active).toBe(2) + + jest.advanceTimersByTime(500) + await Promise.resolve() + await Promise.resolve() + jest.advanceTimersByTime(1000) + await Promise.resolve() + + jest.advanceTimersByTime(2001) + await Promise.resolve() + await Promise.resolve() + + expect(limiter.active).toBe(0) + + expect(mockFn).toHaveBeenCalledTimes(3) + + await operations + jest.useRealTimers() + }) + + it('should wait for processing to complete', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(2, 1000) + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 500)) + return 'result' + }) + + const operation = limiter.exec(mockFn) + const waitPromise = limiter.waitProcessing().then(() => { + console.log('wait complete') + }) + + expect(limiter.active).toBe(1) + + jest.advanceTimersByTime(1001) + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() + + await waitPromise + await operation + expect(limiter.active).toBe(0) + jest.useRealTimers() + }) + + describe('execution tracking', () => { + it('should track running executions correctly', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(3, 1000) + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + return 'result' + }) + + const op1 = limiter.exec(mockFn) + expect(limiter.executions.length).toBe(1) + expect(limiter.executions[0].running).toBe(true) + + const op2 = limiter.exec(mockFn) + expect(limiter.executions.length).toBe(2) + + jest.advanceTimersByTime(101) + await Promise.resolve() + await Promise.resolve() + + await op1 + expect(limiter.executions[0].running).toBe(false) + + await op2 + jest.useRealTimers() + }) + + it('should mark executions as not running after completion', async () => { + const limiter = new TimeRateLimiter(2, 1000) + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return 'result' + }) + + await limiter.exec(mockFn) + + const execution = limiter.executions[0] + expect(execution.running).toBe(false) + }) + + it('should mark executions as not running even on error', async () => { + const limiter = new TimeRateLimiter(2, 1000) + const mockFn = jest.fn().mockRejectedValue(new Error('test error')) + + await expect(limiter.exec(mockFn)).rejects.toThrow('test error') + + const execution = limiter.executions[0] + expect(execution.running).toBe(false) + }) + }) + + describe('cleanup behavior', () => { + it('should cleanup executions older than period', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(3, 1000) + const mockFn = jest.fn().mockResolvedValue('result') + + await limiter.exec(mockFn) + expect(limiter.executions.length).toBe(1) + + jest.advanceTimersByTime(1100) + + await limiter.exec(mockFn) + // After cleanup, only the new execution should remain + expect(limiter.executions.length).toBe(1) + jest.useRealTimers() + }) + + it('should keep running executions regardless of time', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(2, 1000) + let resolveOp: any + const mockFn = jest.fn().mockImplementation( + async () => + await new Promise((resolve) => { + resolveOp = resolve + }) + ) + + const op = limiter.exec(mockFn) + expect(limiter.executions.length).toBe(1) + + jest.advanceTimersByTime(2000) + + // Start another operation to trigger cleanup + const mockFn2 = jest.fn().mockResolvedValue('result') + await limiter.exec(mockFn2) + + // The first operation should still be tracked because it's running + const runningExecution = limiter.executions.find((e) => e.running) + expect(runningExecution).toBeDefined() + + resolveOp('done') + await op + jest.useRealTimers() + }) + }) + + describe('rate limiting behavior', () => { + it('should allow executions up to rate within period', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(3, 1000) + const mockFn = jest.fn().mockResolvedValue('result') + + const ops = [limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn)] + + expect(mockFn).toHaveBeenCalledTimes(3) + await Promise.all(ops) + jest.useRealTimers() + }) + + it('should delay 4th execution when rate is 3', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(3, 1000) + const mockFn = jest.fn().mockResolvedValue('result') + + void limiter.exec(mockFn) + void limiter.exec(mockFn) + void limiter.exec(mockFn) + const fourthOp = limiter.exec(mockFn) + + expect(mockFn).toHaveBeenCalledTimes(3) + + jest.advanceTimersByTime(1001) + await Promise.resolve() + await Promise.resolve() + + await fourthOp + expect(mockFn).toHaveBeenCalledTimes(4) + jest.useRealTimers() + }) + + it('should respect period for rate limiting', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(2, 2000) // 2 per 2 seconds + const mockFn = jest.fn().mockResolvedValue('result') + + void limiter.exec(mockFn) + void limiter.exec(mockFn) + const thirdOp = limiter.exec(mockFn) + + expect(mockFn).toHaveBeenCalledTimes(2) + + // 1 second should not be enough + jest.advanceTimersByTime(1001) + await Promise.resolve() + expect(mockFn).toHaveBeenCalledTimes(2) + + // 2 seconds should allow the third + jest.advanceTimersByTime(1001) + await Promise.resolve() + await Promise.resolve() + + await thirdOp + expect(mockFn).toHaveBeenCalledTimes(3) + jest.useRealTimers() + }) + }) + + describe('error handling', () => { + it('should handle operation errors and continue', async () => { + const limiter = new TimeRateLimiter(2, 1000) + const errorFn = jest.fn().mockRejectedValue(new Error('operation error')) + const successFn = jest.fn().mockResolvedValue('success') + + await expect(limiter.exec(errorFn)).rejects.toThrow('operation error') + expect(limiter.active).toBe(0) + + const result = await limiter.exec(successFn) + expect(result).toBe('success') + }) + + it('should decrement active counter on error', async () => { + const limiter = new TimeRateLimiter(2, 1000) + const mockFn = jest.fn().mockRejectedValue(new Error('test error')) + + expect(limiter.active).toBe(0) + await expect(limiter.exec(mockFn)).rejects.toThrow('test error') + expect(limiter.active).toBe(0) + }) + + it('should handle synchronous throws', async () => { + const limiter = new TimeRateLimiter(2, 1000) + const mockFn = jest.fn().mockImplementation(() => { + throw new Error('sync error') + }) + + await expect(limiter.exec(mockFn)).rejects.toThrow('sync error') + expect(limiter.active).toBe(0) + }) + }) + + describe('waitProcessing', () => { + it('should resolve immediately when no operations are active', async () => { + const limiter = new TimeRateLimiter(2, 1000) + + await limiter.waitProcessing() + expect(limiter.active).toBe(0) + }) + + it('should wait for all active operations to complete', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(2, 1000) + let completed = false + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + completed = true + return 'result' + }) + + void limiter.exec(mockFn) + expect(limiter.active).toBe(1) + + const waitPromise = limiter.waitProcessing() + + jest.advanceTimersByTime(101) + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() + + await waitPromise + expect(completed).toBe(true) + expect(limiter.active).toBe(0) + jest.useRealTimers() + }) + }) + + describe('arguments passing', () => { + it('should pass arguments to operation', async () => { + const limiter = new TimeRateLimiter(2, 1000) + const mockFn = jest.fn().mockImplementation(async (args?: any) => { + return args?.value + }) + + const result = await limiter.exec(mockFn, { value: 42 }) + + expect(result).toBe(42) + expect(mockFn).toHaveBeenCalledWith({ value: 42 }) + }) + + it('should handle operations without arguments', async () => { + const limiter = new TimeRateLimiter(2, 1000) + const mockFn = jest.fn().mockResolvedValue('no-args') + + const result = await limiter.exec(mockFn) + + expect(result).toBe('no-args') + expect(mockFn).toHaveBeenCalledWith(undefined) + }) + }) + + describe('edge cases', () => { + it('should handle rate of 1 correctly', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(1, 1000) + const mockFn = jest.fn().mockResolvedValue('result') + + void limiter.exec(mockFn) + expect(mockFn).toHaveBeenCalledTimes(1) + + const secondOp = limiter.exec(mockFn) + expect(mockFn).toHaveBeenCalledTimes(1) // Should wait + + jest.advanceTimersByTime(1001) + await Promise.resolve() + await Promise.resolve() + + await secondOp + expect(mockFn).toHaveBeenCalledTimes(2) + jest.useRealTimers() + }) + + it('should handle very short period', async () => { + jest.useFakeTimers() + const limiter = new TimeRateLimiter(2, 10) // 2 per 10ms + const mockFn = jest.fn().mockResolvedValue('result') + + void limiter.exec(mockFn) + void limiter.exec(mockFn) + const thirdOp = limiter.exec(mockFn) + + jest.advanceTimersByTime(11) + await Promise.resolve() + + await thirdOp + expect(mockFn).toHaveBeenCalledTimes(3) + jest.useRealTimers() + }) + + it('should handle large rate', async () => { + const limiter = new TimeRateLimiter(100, 1000) + const mockFn = jest.fn().mockResolvedValue('result') + + const operations = Array(50) + .fill(0) + .map(() => limiter.exec(mockFn)) + + await Promise.all(operations) + expect(mockFn).toHaveBeenCalledTimes(50) + }) + }) + + describe('stress test', () => { + it('should handle many operations correctly', async () => { + const limiter = new TimeRateLimiter(10, 100) + const results: number[] = [] + + const mockFn = jest.fn().mockImplementation(async (args?: any) => { + await new Promise((resolve) => setTimeout(resolve, 5)) + results.push(args?.id) + return args?.id + }) + + const operations = Array(30) + .fill(0) + .map((_, i) => limiter.exec(mockFn, { id: i })) + + await Promise.all(operations) + + expect(mockFn).toHaveBeenCalledTimes(30) + expect(results).toHaveLength(30) + expect(limiter.active).toBe(0) + }) + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/memdb.test.ts b/foundations/core/packages/core/src/__tests__/memdb.test.ts new file mode 100644 index 0000000000..9d4f3b1717 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/memdb.test.ts @@ -0,0 +1,408 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Client, type DomainParams, type DomainRequestOptions, type DomainResult } from '..' +import type { Class, Doc, Obj, OperationDomain, Ref } from '../classes' +import core from '../component' +import { Hierarchy } from '../hierarchy' +import { ModelDb, TxDb } from '../memdb' +import { TxOperations } from '../operations' +import { + type DocumentQuery, + type FindOptions, + type SearchOptions, + type SearchQuery, + type SearchResult, + SortingOrder, + type WithLookup +} from '../storage' +import { type Tx } from '../tx' +import { genMinModel, test, type TestMixin } from './minmodel' + +const txes = genMinModel() + +class ClientModel extends ModelDb implements Client { + notify?: ((...tx: Tx[]) => void) | undefined + + getHierarchy (): Hierarchy { + return this.hierarchy + } + + getModel (): ModelDb { + return this + } + + async findOne( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise | undefined> { + return (await this.findAll(_class, query, options)).shift() + } + + async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { + return { docs: [] } + } + + async domainRequest( + domain: OperationDomain, + params: DomainParams, + options?: DomainRequestOptions + ): Promise> { + return { domain, value: null as any } + } + + async close (): Promise {} +} + +async function createModel (modelTxes: Tx[] = txes): Promise<{ model: ClientModel, hierarchy: Hierarchy, txDb: TxDb }> { + const hierarchy = new Hierarchy() + for (const tx of modelTxes) { + hierarchy.tx(tx) + } + const model = new ClientModel(hierarchy) + for (const tx of modelTxes) { + await model.tx(tx) + } + const txDb = new TxDb(hierarchy) + for (const tx of modelTxes) await txDb.tx(tx) + return { model, hierarchy, txDb } +} + +describe('memdb', () => { + it('should save all tx', async () => { + const { txDb } = await createModel() + + const result = await txDb.findAll(core.class.Tx, {}) + expect(result.length).toBe(txes.length) + }) + + it('should create space', async () => { + const { model } = await createModel() + + const client = new TxOperations(model, core.account.System) + const result = await client.findAll(core.class.Space, {}) + expect(result).toHaveLength(2) + + await client.createDoc(core.class.Space, core.space.Model, { + private: false, + name: 'NewSpace', + description: '', + members: [], + archived: false + }) + const result2 = await client.findAll(core.class.Space, {}) + expect(result2).toHaveLength(3) + + await client.createDoc(core.class.Space, core.space.Model, { + private: false, + name: 'NewSpace', + description: '', + members: [], + archived: false + }) + const result3 = await client.findAll(core.class.Space, {}) + expect(result3).toHaveLength(4) + }) + + it('should query model', async () => { + const { model } = await createModel() + const result = await model.findAll(core.class.Class, {}) + const names = result.map((d) => d._id) + expect(names.includes(core.class.Class)).toBe(true) + const result2 = await model.findAll(core.class.Class, { _id: undefined }) + expect(result2.length).toBe(0) + }) + + it('should fail query wrong class', async () => { + const { model } = await createModel() + + await expect(model.findAll('class:workbench.Application' as Ref>, { _id: undefined })).rejects.toThrow() + }) + + it('should create mixin', async () => { + const { model } = await createModel() + const ops = new TxOperations(model, core.account.System) + + await ops.createMixin(core.class.Obj, core.class.Class, core.space.Model, test.mixin.TestMixin, { + arr: ['hello'] + }) + const objClass = (await model.findAll(core.class.Class, { _id: core.class.Obj }))[0] as any + expect(objClass['test:mixin:TestMixin'].arr).toEqual(expect.arrayContaining(['hello'])) + }) + + it('should allow delete', async () => { + const { model } = await createModel() + const result = await model.findAll(core.class.Space, {}) + expect(result.length).toBe(2) + + const ops = new TxOperations(model, core.account.System) + await ops.removeDoc(result[0]._class, result[0].space, result[0]._id) + const result2 = await model.findAll(core.class.Space, {}) + expect(result2).toHaveLength(1) + }) + + it('should query model with params', async () => { + const { model } = await createModel() + const first = await model.findAll(core.class.Class, { + _id: txes[1].objectId as Ref>, + kind: 0 + }) + expect(first.length).toBe(1) + const second = await model.findAll(core.class.Class, { + _id: { $in: [txes[1].objectId as Ref>, txes[3].objectId as Ref>] } + }) + expect(second.length).toBe(2) + const incorrectId = await model.findAll(core.class.Class, { + _id: (txes[1].objectId + 'test') as Ref> + }) + expect(incorrectId.length).toBe(0) + const result = await model.findAll(core.class.Class, { + _id: txes[1].objectId as Ref>, + kind: 1 + }) + expect(result.length).toBe(0) + const multipleParam = await model.findAll(core.class.Doc, { + space: { $in: [core.space.Model, core.space.Tx] } + }) + expect(multipleParam.length).toBeGreaterThan(5) + + const classes = await model.findAll(core.class.Class, {}) + const gt = await model.findAll(core.class.Class, { + kind: { $gt: 1 } + }) + expect(gt.length).toBe(classes.filter((p) => p.kind > 1).length) + const gte = await model.findAll(core.class.Class, { + kind: { $gte: 1 } + }) + expect(gte.length).toBe(classes.filter((p) => p.kind >= 1).length) + const lt = await model.findAll(core.class.Class, { + kind: { $lt: 1 } + }) + expect(lt.length).toBe(classes.filter((p) => p.kind < 1).length) + const lte = await model.findAll(core.class.Class, { + kind: { $lt: 1 } + }) + expect(lte.length).toBe(classes.filter((p) => p.kind <= 1).length) + }) + + it('should query model like params', async () => { + const { model } = await createModel() + const expectedLength = txes.filter((tx) => tx.objectSpace === core.space.Model).length + const without = await model.findAll(core.class.Doc, { + space: { $like: core.space.Model } + }) + expect(without).toHaveLength(expectedLength) + const begin = await model.findAll(core.class.Doc, { + space: { $like: '%Model' } + }) + expect(begin).toHaveLength(expectedLength) + const zero = await model.findAll(core.class.Doc, { + space: { $like: 'Model' } + }) + expect(zero).toHaveLength(0) + const end = await model.findAll(core.class.Doc, { + space: { $like: 'core:space:M%' } + }) + expect(end).toHaveLength(expectedLength) + const mid = await model.findAll(core.class.Doc, { + space: { $like: '%M%de%' } + }) + expect(mid).toHaveLength(expectedLength) + const all = await model.findAll(core.class.Doc, { + space: { $like: '%Mod%' } + }) + expect(all).toHaveLength(expectedLength) + + const regex = await model.findAll(core.class.Doc, { + space: { $regex: '.*Mod.*' } + }) + expect(regex).toHaveLength(expectedLength) + }) + + // TODO: fix this test + // it('should push to array', async () => { + // const hierarchy = new Hierarchy() + // for (const tx of txes) hierarchy.tx(tx) + // const model = new TxOperations(new ClientModel(hierarchy), core.account.System) + // for (const tx of txes) await model.tx(tx) + // const space = await model.createDoc(core.class.Space, core.space.Model, { + // name: 'name', + // description: 'desc', + // private: false, + // members: [], + // archived: false + // }) + // const account = await model.createDoc(core.class.Account, core.space.Model, { + // email: 'email', + // role: AccountRole.User + // }) + // await model.updateDoc(core.class.Space, core.space.Model, space, { $push: { members: account } }) + // const txSpace = await model.findAll(core.class.Space, { _id: space }) + // expect(txSpace[0].members).toEqual(expect.arrayContaining([account])) + // }) + + it('limit and sorting', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + const model = new TxOperations(new ClientModel(hierarchy), core.account.System) + for (const tx of txes) await model.tx(tx) + + const without = await model.findAll(core.class.Space, {}) + expect(without).toHaveLength(2) + + const limit = await model.findAll(core.class.Space, {}, { limit: 1 }) + expect(limit).toHaveLength(1) + + const sortAsc = await model.findAll(core.class.Space, {}, { limit: 1, sort: { name: SortingOrder.Ascending } }) + expect(sortAsc[0].name).toMatch('Sp1') + + const sortDesc = await model.findAll(core.class.Space, {}, { limit: 1, sort: { name: SortingOrder.Descending } }) + expect(sortDesc[0].name).toMatch('Sp2') + + const numberSortDesc = await model.findAll(core.class.Doc, {}, { sort: { modifiedOn: SortingOrder.Descending } }) + expect(numberSortDesc[0].modifiedOn).toBeGreaterThanOrEqual(numberSortDesc[numberSortDesc.length - 1].modifiedOn) + + const numberSort = await model.findAll(core.class.Doc, {}, { sort: { modifiedOn: SortingOrder.Ascending } }) + expect(numberSort[0].modifiedOn).toBeLessThanOrEqual(numberSort[numberSortDesc.length - 1].modifiedOn) + }) + + it('should add attached document', async () => { + const { model } = await createModel() + + const client = new TxOperations(model, core.account.System) + const result = await client.findAll(core.class.Space, {}) + expect(result).toHaveLength(2) + + await client.addCollection(test.class.TestComment, core.space.Model, result[0]._id, result[0]._class, 'comments', { + message: 'msg' + }) + const result2 = await client.findAll(test.class.TestComment, {}) + expect(result2).toHaveLength(1) + }) + + it('lookups', async () => { + const { model } = await createModel() + + const client = new TxOperations(model, core.account.System) + const spaces = await client.findAll(core.class.Space, {}) + expect(spaces).toHaveLength(2) + + const first = await client.addCollection( + test.class.TestComment, + core.space.Model, + spaces[0]._id, + spaces[0]._class, + 'comments', + { + message: 'msg' + } + ) + + const second = await client.addCollection( + test.class.TestComment, + core.space.Model, + first, + test.class.TestComment, + 'comments', + { + message: 'msg2' + } + ) + + await client.addCollection(test.class.TestComment, core.space.Model, spaces[0]._id, spaces[0]._class, 'comments', { + message: 'msg3' + }) + + const simple = await client.findAll( + test.class.TestComment, + { _id: first }, + { lookup: { attachedTo: spaces[0]._class } } + ) + expect(simple[0].$lookup?.attachedTo).toEqual(spaces[0]) + + const nested = await client.findAll( + test.class.TestComment, + { _id: second }, + { lookup: { attachedTo: [test.class.TestComment, { attachedTo: spaces[0]._class } as any] } } + ) + expect((nested[0].$lookup?.attachedTo as any).$lookup?.attachedTo).toEqual(spaces[0]) + + const reverse = await client.findAll( + spaces[0]._class, + { _id: spaces[0]._id }, + { lookup: { _id: { comments: test.class.TestComment } } } + ) + expect((reverse[0].$lookup as any).comments).toHaveLength(2) + }) + + it('mixin lookups', async () => { + const { model } = await createModel() + + const client = new TxOperations(model, core.account.System) + const spaces = await client.findAll(core.class.Space, {}) + expect(spaces).toHaveLength(2) + + const task = await client.createDoc(test.class.Task, spaces[0]._id, { + name: 'TSK1', + number: 1, + state: 0 + }) + + await client.createMixin(task, test.class.Task, spaces[0]._id, test.mixin.TaskMixinTodos, { + todos: 0 + }) + + await client.addCollection(test.class.TestMixinTodo, spaces[0]._id, task, test.mixin.TaskMixinTodos, 'todos', { + text: 'qwe' + }) + await client.addCollection(test.class.TestMixinTodo, spaces[0]._id, task, test.mixin.TaskMixinTodos, 'todos', { + text: 'qwe2' + }) + + const results = await client.findAll( + test.class.TestMixinTodo, + {}, + { lookup: { attachedTo: test.mixin.TaskMixinTodos } } + ) + expect(results.length).toEqual(2) + const attached = results[0].$lookup?.attachedTo + expect(attached).toBeDefined() + expect(Hierarchy.mixinOrClass(attached as Doc)).toEqual(test.mixin.TaskMixinTodos) + }) + + it('createDoc for AttachedDoc', async () => { + expect.assertions(1) + const { model } = await createModel() + + const client = new TxOperations(model, core.account.System) + const spaces = await client.findAll(core.class.Space, {}) + const task = await client.createDoc(test.class.Task, spaces[0]._id, { + name: 'TSK1', + number: 1, + state: 0 + }) + try { + await client.createDoc(test.class.TestMixinTodo, spaces[0]._id, { + text: '', + attachedTo: task, + attachedToClass: test.mixin.TaskMixinTodos, + collection: 'todos' + }) + } catch (e) { + expect(e).toEqual(new Error('createDoc cannot be used for objects inherited from AttachedDoc')) + } + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/minmodel.ts b/foundations/core/packages/core/src/__tests__/minmodel.ts new file mode 100644 index 0000000000..cf9f493126 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/minmodel.ts @@ -0,0 +1,275 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { IntlString, Plugin } from '@hcengineering/platform' +import { plugin } from '@hcengineering/platform' +import type { Arr, AttachedDoc, Class, Data, Doc, Interface, Mixin, Obj, Ref, Space } from '../classes' +import { ClassifierKind, DOMAIN_MODEL } from '../classes' +import core from '../component' +import type { DocumentUpdate, TxCUD, TxCreateDoc, TxRemoveDoc, TxUpdateDoc } from '../tx' +import { DOMAIN_TX, TxFactory } from '../tx' + +const txFactory = new TxFactory(core.account.System) + +function createClass (_class: Ref>, attributes: Data>): TxCreateDoc { + return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class) +} + +function createInterface (_interface: Ref>, attributes: Data>): TxCreateDoc { + return txFactory.createTxCreateDoc(core.class.Interface, core.space.Model, attributes, _interface) +} + +export function createDoc (_class: Ref>, attributes: Data): TxCreateDoc { + return txFactory.createTxCreateDoc(_class, core.space.Model, attributes) +} + +export function updateDoc ( + _class: Ref>, + space: Ref, + objectId: Ref, + operations: DocumentUpdate +): TxUpdateDoc { + return txFactory.createTxUpdateDoc(_class, space, objectId, operations) +} + +export function deleteDoc (_class: Ref>, space: Ref, objectId: Ref): TxRemoveDoc { + return txFactory.createTxRemoveDoc(_class, space, objectId) +} + +export interface TestMixin extends Doc { + arr: Arr +} + +export interface AttachedComment extends AttachedDoc { + message: string +} + +export interface WithState extends Doc { + state: number + number: number +} +export interface Task extends Doc, WithState { + name: string +} + +export interface TaskMixinTodos extends Task { + todos: number +} + +export interface TaskMixinTodo extends AttachedDoc { + text: string +} + +export interface TaskCheckItem extends AttachedDoc, WithState { + name: string + complete: boolean +} + +export const test = plugin('test' as Plugin, { + mixin: { + TestMixin: '' as Ref>, + TaskMixinTodos: '' as Ref> + }, + class: { + Task: '' as Ref>, + TaskCheckItem: '' as Ref>, + TestComment: '' as Ref>, + TestMixinTodo: '' as Ref> + }, + interface: { + WithState: '' as Ref>, + DummyWithState: '' as Ref> + } +}) + +/** + * Generate minimal model for testing purposes. + * @returns R + */ +export function genMinModel (): TxCUD[] { + const txes = [] + // Fill Tx'es with basic model classes. + txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS })) + txes.push( + createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS }) + ) + txes.push( + createClass(core.class.AttachedDoc, { + label: 'AttachedDoc' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.MIXIN + }) + ) + txes.push( + createClass(core.class.Class, { + label: 'Class' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + txes.push( + createClass(core.class.Interface, { + label: 'Interface' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.Space, { + label: 'Space' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + // txes.push( + // createClass(core.class.Account, { + // label: 'Account' as IntlString, + // extends: core.class.Doc, + // kind: ClassifierKind.CLASS, + // domain: DOMAIN_MODEL + // }) + // ) + + txes.push( + createInterface(test.interface.WithState, { + label: 'WithState' as IntlString, + extends: [], + kind: ClassifierKind.INTERFACE + }) + ) + + txes.push( + createClass(core.class.Tx, { + label: 'Tx' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TX + }) + ) + txes.push( + createClass(core.class.TxCUD, { + label: 'TxCUD' as IntlString, + extends: core.class.Tx, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TX + }) + ) + txes.push( + createClass(core.class.TxCreateDoc, { + label: 'TxCreateDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxUpdateDoc, { + label: 'TxUpdateDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxRemoveDoc, { + label: 'TxRemoveDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + + txes.push( + createClass(core.class.Blob, { + label: 'Blob' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS + }) + ) + + txes.push( + createClass(test.mixin.TestMixin, { + label: 'TestMixin' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.MIXIN + }) + ) + + txes.push( + createInterface(test.interface.DummyWithState, { + label: 'DummyWithState' as IntlString, + extends: [test.interface.WithState], + kind: ClassifierKind.INTERFACE + }) + ) + txes.push( + createClass(test.class.TestComment, { + label: 'TestComment' as IntlString, + extends: core.class.AttachedDoc, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(test.class.Task, { + label: 'Task' as IntlString, + extends: core.class.Doc, + implements: [test.interface.DummyWithState], + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(test.class.TaskCheckItem, { + label: 'Task' as IntlString, + extends: core.class.AttachedDoc, + implements: [test.interface.WithState], + kind: ClassifierKind.CLASS + }) + ) + + txes.push( + createClass(test.mixin.TaskMixinTodos, { + label: 'TaskMixinTodos' as IntlString, + extends: test.class.Task, + kind: ClassifierKind.MIXIN + }) + ) + txes.push( + createClass(test.class.TestMixinTodo, { + label: 'TestMixinTodo' as IntlString, + extends: core.class.AttachedDoc, + kind: ClassifierKind.CLASS + }) + ) + + txes.push( + createDoc(core.class.Space, { + name: 'Sp1', + description: '', + private: false, + members: [], + archived: false + }) + ) + + txes.push( + createDoc(core.class.Space, { + name: 'Sp2', + description: '', + private: false, + members: [], + archived: false + }) + ) + return txes +} diff --git a/foundations/core/packages/core/src/__tests__/objvalue.test.ts b/foundations/core/packages/core/src/__tests__/objvalue.test.ts new file mode 100644 index 0000000000..af0be066f7 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/objvalue.test.ts @@ -0,0 +1,290 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { getObjectValue, setObjectValue } from '../objvalue' +import type { Doc } from '../classes' + +describe('objvalue', () => { + describe('getObjectValue', () => { + it('should return the document itself for empty key', () => { + const doc = { _id: '1', name: 'test' } as unknown as Doc + const result = getObjectValue('', doc) + expect(result).toBe(doc) + }) + + it('should get simple property value', () => { + const doc = { _id: '1', name: 'John', age: 30 } as unknown as Doc + expect(getObjectValue('name', doc)).toBe('John') + expect(getObjectValue('age', doc)).toBe(30) + }) + + it('should get nested property value with dot notation', () => { + const doc = { + _id: '1', + user: { + name: 'John', + address: { + city: 'New York', + zip: '10001' + } + } + } as unknown as Doc + + expect(getObjectValue('user.name', doc)).toBe('John') + expect(getObjectValue('user.address.city', doc)).toBe('New York') + expect(getObjectValue('user.address.zip', doc)).toBe('10001') + }) + + it('should handle escaped dollar signs', () => { + const doc = { + _id: '1', + field$name: 'value', + nested: { + prop$test: 123 + } + } as unknown as Doc + + expect(getObjectValue('field\\$name', doc)).toBe('value') + expect(getObjectValue('nested.prop\\$test', doc)).toBe(123) + }) + + it('should return undefined for non-existent properties', () => { + const doc = { _id: '1', name: 'test' } as unknown as Doc + expect(getObjectValue('nonExistent', doc)).toBeUndefined() + expect(getObjectValue('user.name', doc)).toBeUndefined() + }) + + it('should handle array index access', () => { + const doc = { + _id: '1', + items: ['a', 'b', 'c'] + } as unknown as Doc + + expect(getObjectValue('items.0', doc)).toBe('a') + expect(getObjectValue('items.1', doc)).toBe('b') + expect(getObjectValue('items.2', doc)).toBe('c') + }) + + it('should handle nested arrays with named properties', () => { + const doc = { + _id: '1', + users: [ + { name: 'Alice', age: 25 }, + { name: 'Bob', age: 30 } + ] + } as unknown as Doc + + const names = getObjectValue('users.name', doc) + expect(names).toEqual(['Alice', 'Bob']) + }) + + it('should handle deeply nested array queries', () => { + const doc = { + _id: '1', + departments: [ + { + name: 'Engineering', + employees: [ + { name: 'Alice', role: 'Dev' }, + { name: 'Bob', role: 'QA' } + ] + }, + { + name: 'Sales', + employees: [{ name: 'Charlie', role: 'Manager' }] + } + ] + } as unknown as Doc + + const employeeNames = getObjectValue('departments.employees.name', doc) + expect(employeeNames).toEqual(['Alice', 'Bob', 'Charlie']) + }) + + it('should handle null and undefined values in path', () => { + const doc = { + _id: '1', + user: null, + data: undefined + } as unknown as Doc + + expect(getObjectValue('user.name', doc)).toBeUndefined() + expect(getObjectValue('data.value', doc)).toBeUndefined() + }) + + it('should handle mixed nested structures', () => { + const doc = { + _id: '1', + config: { + settings: { + theme: 'dark', + notifications: true + } + } + } as unknown as Doc + + expect(getObjectValue('config.settings.theme', doc)).toBe('dark') + expect(getObjectValue('config.settings.notifications', doc)).toBe(true) + }) + + it('should return array for nested array property queries', () => { + const doc = { + _id: '1', + teams: [{ members: [{ name: 'A' }, { name: 'B' }] }, { members: [{ name: 'C' }] }] + } as unknown as Doc + + const memberNames = getObjectValue('teams.members.name', doc) + expect(memberNames).toEqual(['A', 'B', 'C']) + }) + }) + + describe('setObjectValue', () => { + it('should not do anything for empty key', () => { + const doc = { _id: '1', name: 'test' } as unknown as Doc + setObjectValue('', doc, 'newValue') + expect(doc).toEqual({ _id: '1', name: 'test' }) + }) + + it('should set simple property value', () => { + const doc = { _id: '1', name: 'old' } as unknown as Doc + setObjectValue('name', doc, 'new') + expect((doc as any).name).toBe('new') + }) + + it('should set nested property value', () => { + const doc = { + _id: '1', + user: { + name: 'old' + } + } as unknown as Doc + + setObjectValue('user.name', doc, 'new') + expect((doc as any).user.name).toBe('new') + }) + + it('should create nested objects if they do not exist', () => { + const doc = { _id: '1' } as unknown as Doc + setObjectValue('user.profile.name', doc, 'John') + expect((doc as any).user.profile.name).toBe('John') + }) + + it('should handle escaped dollar signs', () => { + const doc = { _id: '1' } as unknown as Doc + setObjectValue('field\\$name', doc, 'value') + expect((doc as any).field$name).toBe('value') + }) + + it('should clone the value before setting', () => { + const doc = { _id: '1' } as unknown as Doc + const value = { nested: 'object' } + setObjectValue('data', doc, value) + + // Modify original value + value.nested = 'modified' + + // Doc should have the original value (cloned) + expect((doc as any).data.nested).toBe('object') + }) + + it('should create nested objects even when intermediate value is array', () => { + const doc = { + _id: '1', + items: [{ name: 'a' }, { name: 'b' }] + } as unknown as Doc + + // This actually doesn't throw - it creates a 'name' property on the array object + setObjectValue('items.name', doc, 'new') + expect((doc as any).items.name).toBe('new') + }) + + it('should set value in existing nested structure', () => { + const doc = { + _id: '1', + config: { + settings: { + theme: 'dark' + } + } + } as unknown as Doc + + setObjectValue('config.settings.theme', doc, 'light') + expect((doc as any).config.settings.theme).toBe('light') + }) + + it('should handle setting multiple levels', () => { + const doc = { _id: '1' } as unknown as Doc + + setObjectValue('a.b.c.d', doc, 'deep') + expect((doc as any).a.b.c.d).toBe('deep') + }) + + it('should set numeric values', () => { + const doc = { _id: '1' } as unknown as Doc + setObjectValue('count', doc, 42) + expect((doc as any).count).toBe(42) + }) + + it('should set boolean values', () => { + const doc = { _id: '1' } as unknown as Doc + setObjectValue('enabled', doc, true) + expect((doc as any).enabled).toBe(true) + }) + + it('should set null values', () => { + const doc = { _id: '1', data: 'old' } as unknown as Doc + setObjectValue('data', doc, null) + expect((doc as any).data).toBe(null) + }) + + it('should overwrite existing nested structures', () => { + const doc = { + _id: '1', + config: { + old: 'value', + nested: { prop: 'test' } + } + } as unknown as Doc + + setObjectValue('config', doc, { new: 'structure' }) + expect((doc as any).config).toEqual({ new: 'structure' }) + }) + + it('should handle array values', () => { + const doc = { _id: '1' } as unknown as Doc + const arrayValue = [1, 2, 3] + setObjectValue('numbers', doc, arrayValue) + + // Modify original array + arrayValue.push(4) + + // Doc should have cloned array + expect((doc as any).numbers).toEqual([1, 2, 3]) + }) + + it('should handle object values', () => { + const doc = { _id: '1' } as unknown as Doc + setObjectValue('data', doc, { key: 'value', nested: { prop: 123 } }) + expect((doc as any).data).toEqual({ key: 'value', nested: { prop: 123 } }) + }) + + it('should create intermediate objects when needed', () => { + const doc = { _id: '1', existing: 'prop' } as unknown as Doc + setObjectValue('new.path.value', doc, 'data') + expect((doc as any).new.path.value).toBe('data') + expect((doc as any).existing).toBe('prop') + }) + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/operator-bugs.test.ts b/foundations/core/packages/core/src/__tests__/operator-bugs.test.ts new file mode 100644 index 0000000000..50e0744d0c --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/operator-bugs.test.ts @@ -0,0 +1,245 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { _getOperator } from '../operator' +import type { Doc } from '../classes' + +describe('operator edge cases and potential bugs', () => { + describe('$push operator edge cases', () => { + it('BUG: $push with $each on null field should handle gracefully', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: null } as unknown as Doc + const operator = _getOperator('$push') + + // This exposes a bug: when arr is null and we use $each, nothing happens + operator(doc, { arr: { $each: [1, 2, 3] } }) + + // Expected: should initialize array and add items + // Actual: arr remains null (bug!) + // Uncomment to see the bug: + // expect((doc as any).arr).toEqual([1, 2, 3]) + + // Current behavior (documents the bug): + expect((doc as any).arr).toBe(null) + }) + + it('BUG: $push with $each on non-array field should handle gracefully', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: 'string' } as unknown as Doc + const operator = _getOperator('$push') + + // This exposes a bug: when arr is a non-array and we use $each, nothing happens + operator(doc, { arr: { $each: [1, 2, 3] } }) + + // Expected: should report error and replace with array + // Actual: arr remains a string (bug!) + // Uncomment to see the bug: + // expect((doc as any).arr).toEqual([1, 2, 3]) + + // Current behavior (documents the bug): + expect((doc as any).arr).toBe('string') + }) + + it('$push without $each on null field works correctly', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: null } as unknown as Doc + const operator = _getOperator('$push') + + operator(doc, { arr: 'value' }) + + // This works correctly - replaces null with array + expect((doc as any).arr).toEqual(['value']) + }) + + it('$push without $each on non-array field reports error and fixes', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: 'string' } as unknown as Doc + const operator = _getOperator('$push') + + // This should log an error via Analytics.handleError + operator(doc, { arr: 'value' }) + + // This works - replaces non-array with array + expect((doc as any).arr).toEqual(['value']) + }) + }) + + describe('$pull operator edge cases', () => { + it('should initialize undefined field as empty array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc + const operator = _getOperator('$pull') + + operator(doc, { arr: 'value' }) + + expect((doc as any).arr).toEqual([]) + }) + + it('should handle $in with empty array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2, 3] } as unknown as Doc + const operator = _getOperator('$pull') + + operator(doc, { arr: { $in: [] } }) + + // Nothing should be removed + expect((doc as any).arr).toEqual([1, 2, 3]) + }) + + it('should handle complex objects with partial matches', () => { + const doc: Doc = { + _id: '1' as any, + _class: 'test' as any, + arr: [ + { a: 1, b: 2, c: 3 }, + { a: 1, b: 2, c: 4 }, + { a: 2, b: 2, c: 3 } + ] + } as unknown as Doc + + const operator = _getOperator('$pull') + + // Should only remove objects where ALL fields match + operator(doc, { arr: { a: 1, b: 2 } }) + + // Neither object should be removed because c doesn't match + expect((doc as any).arr).toEqual([{ a: 2, b: 2, c: 3 }]) + }) + }) + + describe('$update operator edge cases', () => { + it('should handle query with multiple conditions', () => { + const doc: Doc = { + _id: '1' as any, + _class: 'test' as any, + arr: [ + { name: 'a', status: 'active', value: 1 }, + { name: 'a', status: 'inactive', value: 2 }, + { name: 'b', status: 'active', value: 3 } + ] + } as unknown as Doc + + const operator = _getOperator('$update') + + operator(doc, { + arr: { + $query: { name: 'a', status: 'active' }, + $update: { value: 100 } + } + }) + + expect((doc as any).arr).toEqual([ + { name: 'a', status: 'active', value: 100 }, + { name: 'a', status: 'inactive', value: 2 }, + { name: 'b', status: 'active', value: 3 } + ]) + }) + + it('should add new fields in update', () => { + const doc: Doc = { + _id: '1' as any, + _class: 'test' as any, + arr: [{ name: 'a' }] + } as unknown as Doc + + const operator = _getOperator('$update') + + operator(doc, { + arr: { + $query: { name: 'a' }, + $update: { newField: 'newValue' } + } + }) + + expect((doc as any).arr).toEqual([{ name: 'a', newField: 'newValue' }]) + }) + }) + + describe('$inc operator edge cases', () => { + it('should handle floating point numbers', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, value: 1.5 } as unknown as Doc + const operator = _getOperator('$inc') + + operator(doc, { value: 2.3 }) + + expect((doc as any).value).toBeCloseTo(3.8) + }) + + it('should handle very large numbers', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, value: Number.MAX_SAFE_INTEGER - 1 } as unknown as Doc + const operator = _getOperator('$inc') + + operator(doc, { value: 1 }) + + expect((doc as any).value).toBe(Number.MAX_SAFE_INTEGER) + }) + + it('should handle negative numbers to zero', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 5 } as unknown as Doc + const operator = _getOperator('$inc') + + operator(doc, { count: -5 }) + + expect((doc as any).count).toBe(0) + }) + }) + + describe('$rename operator edge cases', () => { + it('BUG: should handle renaming to the same name causes deletion', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 'value' } as unknown as Doc + const operator = _getOperator('$rename') + + operator(doc, { field: 'field' }) + + // BUG: Renaming to same name causes deletion! + // The implementation deletes first, then sets, but uses the same key + expect((doc as any).field).toBeUndefined() + }) + + it('BUG: chain renaming has race condition', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, a: 1, b: 2, c: 3 } as unknown as Doc + const operator = _getOperator('$rename') + + // BUG: This creates a potential issue with chained renames in same operation + operator(doc, { a: 'b', b: 'c' }) + + // The order of operations in the for loop determines outcome + // a -> b happens first (sets b to 1, deletes a) + // b -> c happens second (sets c to 1 [current value of b], deletes b) + expect((doc as any).a).toBeUndefined() + expect((doc as any).b).toBeUndefined() + expect((doc as any).c).toBe(1) // Gets the value that was assigned to b from a + }) + }) + + describe('$unset operator edge cases', () => { + it('should handle unsetting nested properties (only top level)', () => { + const doc: Doc = { + _id: '1' as any, + _class: 'test' as any, + obj: { nested: 'value', other: 'data' } + } as unknown as Doc + const operator = _getOperator('$unset') + + // $unset only works at the key level, not nested paths + operator(doc, { obj: '' }) + + expect((doc as any).obj).toBeUndefined() + }) + + it('should handle unsetting array elements', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2, 3] } as unknown as Doc + const operator = _getOperator('$unset') + + operator(doc, { arr: '' }) + + expect((doc as any).arr).toBeUndefined() + }) + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/operator.test.ts b/foundations/core/packages/core/src/__tests__/operator.test.ts new file mode 100644 index 0000000000..a3b6d7adad --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/operator.test.ts @@ -0,0 +1,535 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { _getOperator, isOperator } from '../operator' +import type { Doc } from '../classes' + +describe('operator', () => { + describe('isOperator', () => { + it('should return true for operator objects', () => { + expect(isOperator({ $push: { arr: 1 } })).toBe(true) + expect(isOperator({ $pull: { arr: 1 } })).toBe(true) + expect(isOperator({ $inc: { count: 1 } })).toBe(true) + expect(isOperator({ $unset: { field: '' } })).toBe(true) + expect(isOperator({ $rename: { oldField: 'newField' } })).toBe(true) + expect(isOperator({ $update: { arr: {} } })).toBe(true) + }) + + it('should return true for multiple operators', () => { + expect(isOperator({ $push: { arr: 1 }, $inc: { count: 1 } })).toBe(true) + }) + + it('should return false for non-operator objects', () => { + expect(isOperator({ field: 'value' })).toBe(false) + expect(isOperator({ _id: '123' })).toBe(false) + expect(isOperator({ name: 'test', value: 123 })).toBe(false) + }) + + it('should return false for empty objects', () => { + expect(isOperator({})).toBe(false) + }) + + it('should return false for null or non-objects', () => { + expect(isOperator(null as any)).toBe(false) + expect(isOperator(undefined as any)).toBe(false) + expect(isOperator('string' as any)).toBe(false) + expect(isOperator(123 as any)).toBe(false) + expect(isOperator([] as any)).toBe(false) + }) + + it('should return false for mixed operator and non-operator keys', () => { + expect(isOperator({ $push: { arr: 1 }, field: 'value' })).toBe(false) + }) + }) + + describe('$push operator', () => { + it('should push a single value to an array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc + const operator = _getOperator('$push') + operator(doc, { arr: 'value' }) + expect((doc as any).arr).toEqual(['value']) + }) + + it('should push multiple values with $each', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2] } as unknown as Doc + const operator = _getOperator('$push') + operator(doc, { arr: { $each: [3, 4, 5] } }) + expect((doc as any).arr).toEqual([3, 4, 5, 1, 2]) + }) + + it('should push with $position', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2, 3] } as unknown as Doc + const operator = _getOperator('$push') + operator(doc, { arr: { $each: [10, 20], $position: 1 } }) + expect((doc as any).arr).toEqual([1, 10, 20, 2, 3]) + }) + + it('should push object to array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [] } as unknown as Doc + const operator = _getOperator('$push') + const obj = { name: 'test', value: 123 } + operator(doc, { arr: obj }) + expect((doc as any).arr).toEqual([obj]) + }) + + it('should initialize undefined field as array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc + const operator = _getOperator('$push') + operator(doc, { newArr: 'value' }) + expect((doc as any).newArr).toEqual(['value']) + }) + + it('should handle null array field', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: null } as any + const operator = _getOperator('$push') + operator(doc, { arr: 'value' }) + expect((doc as any).arr).toEqual(['value']) + }) + + it('should handle pushing to non-array field', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 'string' } as unknown as Doc + const operator = _getOperator('$push') + operator(doc, { field: 'value' }) + // Should replace non-array with array + expect((doc as any).field).toEqual(['value']) + }) + }) + + describe('$pull operator', () => { + it('should pull a single value from array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2, 3, 2] } as unknown as Doc + const operator = _getOperator('$pull') + operator(doc, { arr: 2 }) + expect((doc as any).arr).toEqual([1, 3]) + }) + + it('should pull multiple values with $in', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2, 3, 4, 5] } as unknown as Doc + const operator = _getOperator('$pull') + operator(doc, { arr: { $in: [2, 4] } }) + expect((doc as any).arr).toEqual([1, 3, 5]) + }) + + it('should pull objects matching all fields', () => { + const doc: Doc = { + _id: '1' as any, + _class: 'test' as any, + arr: [ + { name: 'a', value: 1 }, + { name: 'b', value: 2 }, + { name: 'a', value: 3 } + ] + } as unknown as Doc + const operator = _getOperator('$pull') + operator(doc, { arr: { name: 'a', value: 1 } }) + expect((doc as any).arr).toEqual([ + { name: 'b', value: 2 }, + { name: 'a', value: 3 } + ]) + }) + + it('should handle undefined field', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc + const operator = _getOperator('$pull') + operator(doc, { arr: 'value' }) + expect((doc as any).arr).toEqual([]) + }) + + it('should handle null array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: null } as any + const operator = _getOperator('$pull') + operator(doc, { arr: 'value' }) + expect((doc as any).arr).toEqual([]) + }) + + it('should not pull when object fields do not all match', () => { + const doc: Doc = { + _id: '1' as any, + _class: 'test' as any, + arr: [ + { name: 'a', value: 1 }, + { name: 'b', value: 2 } + ] + } as unknown as Doc + const operator = _getOperator('$pull') + operator(doc, { arr: { name: 'a', value: 999 } }) + expect((doc as any).arr).toEqual([ + { name: 'a', value: 1 }, + { name: 'b', value: 2 } + ]) + }) + }) + + describe('$update operator', () => { + it('should update matching array elements', () => { + const doc: Doc = { + _id: '1' as any, + _class: 'test' as any, + arr: [ + { name: 'a', value: 1 }, + { name: 'b', value: 2 }, + { name: 'a', value: 3 } + ] + } as unknown as Doc + const operator = _getOperator('$update') + operator(doc, { + arr: { + $query: { name: 'a' }, + $update: { value: 100 } + } + }) + expect((doc as any).arr).toEqual([ + { name: 'a', value: 100 }, + { name: 'b', value: 2 }, + { name: 'a', value: 100 } + ]) + }) + + it('should handle empty array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [] } as unknown as Doc + const operator = _getOperator('$update') + operator(doc, { + arr: { + $query: { name: 'a' }, + $update: { value: 100 } + } + }) + expect((doc as any).arr).toEqual([]) + }) + + it('should handle undefined field', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc + const operator = _getOperator('$update') + operator(doc, { + arr: { + $query: { name: 'a' }, + $update: { value: 100 } + } + }) + expect((doc as any).arr).toEqual([]) + }) + + it('should update multiple fields', () => { + const doc: Doc = { + _id: '1' as any, + _class: 'test' as any, + arr: [{ name: 'a', value: 1, extra: 'old' }] + } as unknown as Doc + const operator = _getOperator('$update') + operator(doc, { + arr: { + $query: { name: 'a' }, + $update: { value: 100, extra: 'new' } + } + }) + expect((doc as any).arr).toEqual([{ name: 'a', value: 100, extra: 'new' }]) + }) + + it('should not update non-matching elements', () => { + const doc: Doc = { + _id: '1' as any, + _class: 'test' as any, + arr: [ + { name: 'a', value: 1 }, + { name: 'b', value: 2 } + ] + } as unknown as Doc + const operator = _getOperator('$update') + operator(doc, { + arr: { + $query: { name: 'c' }, + $update: { value: 100 } + } + }) + expect((doc as any).arr).toEqual([ + { name: 'a', value: 1 }, + { name: 'b', value: 2 } + ]) + }) + }) + + describe('$inc operator', () => { + it('should increment existing number', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 5 } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: 3 }) + expect((doc as any).count).toBe(8) + }) + + it('should initialize undefined field to increment value', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: 10 }) + expect((doc as any).count).toBe(10) + }) + + it('should handle negative increment', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: -3 }) + expect((doc as any).count).toBe(7) + }) + + it('should increment multiple fields', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, a: 1, b: 2 } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { a: 10, b: 20 }) + expect((doc as any).a).toBe(11) + expect((doc as any).b).toBe(22) + }) + + it('should handle zero increment', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 5 } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: 0 }) + expect((doc as any).count).toBe(5) + }) + }) + + describe('$unset operator', () => { + it('should remove existing field', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 'value' } as unknown as Doc + const operator = _getOperator('$unset') + operator(doc, { field: '' }) + expect((doc as any).field).toBeUndefined() + }) + + it('should handle undefined field', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc + const operator = _getOperator('$unset') + operator(doc, { field: '' }) + expect((doc as any).field).toBeUndefined() + }) + + it('should unset multiple fields', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, a: 1, b: 2, c: 3 } as unknown as Doc + const operator = _getOperator('$unset') + operator(doc, { a: '', c: '' }) + expect((doc as any).a).toBeUndefined() + expect((doc as any).b).toBe(2) + expect((doc as any).c).toBeUndefined() + }) + + it('should unset nested objects', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, obj: { nested: 'value' } } as unknown as Doc + const operator = _getOperator('$unset') + operator(doc, { obj: '' }) + expect((doc as any).obj).toBeUndefined() + }) + }) + + describe('$rename operator', () => { + it('should rename existing field', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, oldName: 'value' } as unknown as Doc + const operator = _getOperator('$rename') + operator(doc, { oldName: 'newName' }) + expect((doc as any).oldName).toBeUndefined() + expect((doc as any).newName).toBe('value') + }) + + it('should handle undefined field', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc + const operator = _getOperator('$rename') + operator(doc, { oldName: 'newName' }) + expect((doc as any).oldName).toBeUndefined() + expect((doc as any).newName).toBeUndefined() + }) + + it('should rename multiple fields', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, a: 1, b: 2 } as unknown as Doc + const operator = _getOperator('$rename') + operator(doc, { a: 'x', b: 'y' }) + expect((doc as any).a).toBeUndefined() + expect((doc as any).b).toBeUndefined() + expect((doc as any).x).toBe(1) + expect((doc as any).y).toBe(2) + }) + + it('should overwrite existing field with same name', () => { + const doc: Doc = { + _id: '1' as any, + _class: 'test' as any, + old: 'oldValue', + new: 'existingValue' + } as unknown as Doc + const operator = _getOperator('$rename') + operator(doc, { old: 'new' }) + expect((doc as any).old).toBeUndefined() + expect((doc as any).new).toBe('oldValue') + }) + + it('should preserve object references when renaming', () => { + const obj = { nested: 'value' } + const doc: Doc = { _id: '1' as any, _class: 'test' as any, oldName: obj } as unknown as Doc + const operator = _getOperator('$rename') + operator(doc, { oldName: 'newName' }) + expect((doc as any).newName).toBe(obj) + }) + }) + + describe('_getOperator', () => { + it('should return correct operator functions', () => { + expect(_getOperator('$push')).toBeDefined() + expect(_getOperator('$pull')).toBeDefined() + expect(_getOperator('$update')).toBeDefined() + expect(_getOperator('$inc')).toBeDefined() + expect(_getOperator('$unset')).toBeDefined() + expect(_getOperator('$rename')).toBeDefined() + }) + + it('should throw error for unknown operator', () => { + expect(() => _getOperator('$unknown')).toThrow('unknown operator: $unknown') + expect(() => _getOperator('$invalid')).toThrow('unknown operator: $invalid') + }) + }) + + describe('operator error handling', () => { + describe('$pull with non-array values', () => { + it('should handle string value instead of array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 'not-an-array' } as unknown as Doc + const operator = _getOperator('$pull') + operator(doc, { field: 'value' }) + expect((doc as any).field).toEqual([]) + }) + + it('should handle object value instead of array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: { key: 'value' } } as unknown as Doc + const operator = _getOperator('$pull') + operator(doc, { field: 'value' }) + expect((doc as any).field).toEqual([]) + }) + + it('should handle number value instead of array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 42 } as unknown as Doc + const operator = _getOperator('$pull') + operator(doc, { field: 'value' }) + expect((doc as any).field).toEqual([]) + }) + + it('should handle null value', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: null } as unknown as Doc + const operator = _getOperator('$pull') + operator(doc, { field: 'value' }) + expect((doc as any).field).toEqual([]) + }) + + it('should work correctly with valid array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: [1, 2, 3] } as unknown as Doc + const operator = _getOperator('$pull') + operator(doc, { field: 2 }) + expect((doc as any).field).toEqual([1, 3]) + }) + }) + + describe('$update with non-array values', () => { + it('should handle string value instead of array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 'not-an-array' } as unknown as Doc + const operator = _getOperator('$update') + operator(doc, { field: { $query: { id: 1 }, $update: { value: 'new' } } }) + expect((doc as any).field).toEqual([]) + }) + + it('should handle object value instead of array', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: { key: 'value' } } as unknown as Doc + const operator = _getOperator('$update') + operator(doc, { field: { $query: { id: 1 }, $update: { value: 'new' } } }) + expect((doc as any).field).toEqual([]) + }) + + it('should handle null value', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: null } as unknown as Doc + const operator = _getOperator('$update') + operator(doc, { field: { $query: { id: 1 }, $update: { value: 'new' } } }) + expect((doc as any).field).toEqual([]) + }) + + it('should work correctly with valid array', () => { + const doc: Doc = { + _id: '1' as any, + _class: 'test' as any, + field: [ + { id: 1, value: 'old' }, + { id: 2, value: 'keep' } + ] + } as unknown as Doc + const operator = _getOperator('$update') + operator(doc, { field: { $query: { id: 1 }, $update: { value: 'new' } } }) + expect((doc as any).field).toEqual([ + { id: 1, value: 'new' }, + { id: 2, value: 'keep' } + ]) + }) + }) + + describe('$inc with non-numeric values', () => { + it('should handle NaN current value', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: NaN } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: 5 }) + expect((doc as any).count).toBe(5) + }) + + it('should handle string current value', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 'not-a-number' } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: 5 }) + expect((doc as any).count).toBe(5) + }) + + it('should handle NaN increment value', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: NaN }) + expect((doc as any).count).toBe(10) + }) + + it('should handle string increment value', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: 'not-a-number' as any }) + expect((doc as any).count).toBe(10) + }) + + it('should handle undefined current value (should default to 0)', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: 5 }) + expect((doc as any).count).toBe(5) + }) + + it('should work correctly with valid numbers', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: 5 }) + expect((doc as any).count).toBe(15) + }) + + it('should handle negative increments', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: -3 }) + expect((doc as any).count).toBe(7) + }) + + it('should handle zero increment', () => { + const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc + const operator = _getOperator('$inc') + operator(doc, { count: 0 }) + expect((doc as any).count).toBe(10) + }) + }) + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/rate-limiter.test.ts b/foundations/core/packages/core/src/__tests__/rate-limiter.test.ts new file mode 100644 index 0000000000..5ae22898a3 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/rate-limiter.test.ts @@ -0,0 +1,449 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { RateLimiter } from '../utils' + +describe('RateLimiter', () => { + describe('constructor', () => { + it('should create limiter with specified rate', () => { + const limiter = new RateLimiter(5) + expect(limiter.rate).toBe(5) + expect(limiter.idCounter).toBe(0) + expect(limiter.processingQueue.size).toBe(0) + }) + + it('should handle rate of 1', () => { + const limiter = new RateLimiter(1) + expect(limiter.rate).toBe(1) + }) + + it('should handle large rates', () => { + const limiter = new RateLimiter(1000) + expect(limiter.rate).toBe(1000) + }) + }) + + describe('exec', () => { + it('should execute single operation immediately', async () => { + const limiter = new RateLimiter(1) + const mockFn = jest.fn().mockResolvedValue('result') + + const result = await limiter.exec(mockFn) + + expect(mockFn).toHaveBeenCalledTimes(1) + expect(result).toBe('result') + expect(limiter.processingQueue.size).toBe(0) + }) + + it('should execute operations up to rate limit concurrently', async () => { + const limiter = new RateLimiter(3) + let runningCount = 0 + let maxRunning = 0 + + const mockFn = jest.fn().mockImplementation(async () => { + runningCount++ + maxRunning = Math.max(maxRunning, runningCount) + await new Promise((resolve) => setTimeout(resolve, 10)) + runningCount-- + return 'result' + }) + + const operations = [ + limiter.exec(mockFn), + limiter.exec(mockFn), + limiter.exec(mockFn), + limiter.exec(mockFn), + limiter.exec(mockFn) + ] + + await Promise.all(operations) + + expect(mockFn).toHaveBeenCalledTimes(5) + expect(maxRunning).toBeLessThanOrEqual(3) + }) + + it('should queue operations beyond rate limit', async () => { + const limiter = new RateLimiter(2) + const order: number[] = [] + + const createOperation = (id: number) => async () => { + order.push(id) + await new Promise((resolve) => setTimeout(resolve, 10)) + return id + } + + const operations = [ + limiter.exec(createOperation(1)), + limiter.exec(createOperation(2)), + limiter.exec(createOperation(3)), + limiter.exec(createOperation(4)) + ] + + await Promise.all(operations) + + expect(order).toEqual([1, 2, 3, 4]) + }) + + it('should handle operation errors correctly', async () => { + const limiter = new RateLimiter(2) + const mockFn = jest.fn().mockRejectedValue(new Error('Operation failed')) + + await expect(limiter.exec(mockFn)).rejects.toThrow('Operation failed') + expect(limiter.processingQueue.size).toBe(0) + }) + + it('should continue processing after error', async () => { + const limiter = new RateLimiter(1) + const successFn = jest.fn().mockResolvedValue('success') + const errorFn = jest.fn().mockRejectedValue(new Error('error')) + + await expect(limiter.exec(errorFn)).rejects.toThrow('error') + const result = await limiter.exec(successFn) + + expect(result).toBe('success') + expect(successFn).toHaveBeenCalledTimes(1) + }) + + it('should pass arguments to operations', async () => { + const limiter = new RateLimiter(1) + const mockFn = jest.fn().mockImplementation(async (args?: any) => { + return args?.value + }) + + const result = await limiter.exec(mockFn, { value: 42 }) + + expect(result).toBe(42) + expect(mockFn).toHaveBeenCalledWith({ value: 42 }) + }) + + it('should handle operations without arguments', async () => { + const limiter = new RateLimiter(1) + const mockFn = jest.fn().mockResolvedValue('no-args') + + const result = await limiter.exec(mockFn) + + expect(result).toBe('no-args') + expect(mockFn).toHaveBeenCalledWith(undefined) + }) + + it('should increment idCounter for each operation', async () => { + const limiter = new RateLimiter(10) + const mockFn = jest.fn().mockResolvedValue('result') + + await limiter.exec(mockFn) + expect(limiter.idCounter).toBe(1) + + await limiter.exec(mockFn) + expect(limiter.idCounter).toBe(2) + + await limiter.exec(mockFn) + expect(limiter.idCounter).toBe(3) + }) + + it('should notify waiting operations when slots become available', async () => { + const limiter = new RateLimiter(1) + const order: string[] = [] + + const op1 = async (): Promise => { + order.push('op1-start') + await new Promise((resolve) => setTimeout(resolve, 20)) + order.push('op1-end') + return 'op1' + } + + const op2 = async (): Promise => { + order.push('op2-start') + await new Promise((resolve) => setTimeout(resolve, 10)) + order.push('op2-end') + return 'op2' + } + + const results = await Promise.all([limiter.exec(op1), limiter.exec(op2)]) + + expect(results).toEqual(['op1', 'op2']) + expect(order).toEqual(['op1-start', 'op1-end', 'op2-start', 'op2-end']) + }) + + it('should handle rapid sequential operations', async () => { + const limiter = new RateLimiter(2) + const mockFn = jest.fn().mockResolvedValue('result') + const results = [] + + for (let i = 0; i < 10; i++) { + results.push(limiter.exec(mockFn)) + } + + await Promise.all(results) + expect(mockFn).toHaveBeenCalledTimes(10) + }) + + it('should clean up processingQueue after completion', async () => { + const limiter = new RateLimiter(5) + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 5)) + return 'result' + }) + + const operations = Array(5) + .fill(0) + .map(() => limiter.exec(mockFn)) + + await Promise.all(operations) + expect(limiter.processingQueue.size).toBe(0) + }) + }) + + describe('add', () => { + it('should add operation without waiting for result', async () => { + const limiter = new RateLimiter(1) + let executed = false + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + executed = true + return 'result' + }) + + await limiter.add(mockFn) + + // add returns immediately, but operation may not be complete + expect(mockFn).toHaveBeenCalledTimes(1) + + // Wait for operation to complete + await new Promise((resolve) => setTimeout(resolve, 20)) + expect(executed).toBe(true) + }) + + it('should handle errors with error handler', async () => { + const limiter = new RateLimiter(1) + const errorHandler = jest.fn() + const mockFn = jest.fn().mockRejectedValue(new Error('test error')) + + await limiter.add(mockFn, undefined, errorHandler) + + // Wait for operation to execute + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(errorHandler).toHaveBeenCalledWith(new Error('test error')) + }) + + it('should log errors when no error handler provided', async () => { + const limiter = new RateLimiter(1) + const consoleSpy = jest.spyOn(console, 'error').mockImplementation() + const mockFn = jest.fn().mockRejectedValue(new Error('test error')) + + await limiter.add(mockFn) + + // Wait for operation to execute + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(consoleSpy).toHaveBeenCalled() + consoleSpy.mockRestore() + }) + + it('should pass arguments to operation', async () => { + const limiter = new RateLimiter(1) + const mockFn = jest.fn().mockResolvedValue('result') + + await limiter.add(mockFn, { test: 'value' }) + + // Wait for operation to start + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(mockFn).toHaveBeenCalledWith({ test: 'value' }) + }) + + it('should queue multiple add operations', async () => { + const limiter = new RateLimiter(1) + const order: number[] = [] + + const createOp = (id: number) => async () => { + order.push(id) + await new Promise((resolve) => setTimeout(resolve, 10)) + } + + await limiter.add(createOp(1)) + await limiter.add(createOp(2)) + await limiter.add(createOp(3)) + + // Wait for all operations to complete + await limiter.waitProcessing() + + expect(order).toEqual([1, 2, 3]) + }) + }) + + describe('waitProcessing', () => { + it('should wait for all operations to complete', async () => { + const limiter = new RateLimiter(2) + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 20)) + return 'result' + }) + + const operations = [limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn)] + + expect(limiter.processingQueue.size).toBeGreaterThan(0) + + await limiter.waitProcessing() + + expect(limiter.processingQueue.size).toBe(0) + await Promise.all(operations) + }) + + it('should resolve immediately when no operations are processing', async () => { + const limiter = new RateLimiter(1) + + await limiter.waitProcessing() + + expect(limiter.processingQueue.size).toBe(0) + }) + + it('should wait for operations added via add method', async () => { + const limiter = new RateLimiter(1) + let completed = false + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 20)) + completed = true + }) + + await limiter.add(mockFn) + expect(completed).toBe(false) + + await limiter.waitProcessing() + expect(completed).toBe(true) + }) + }) + + describe('edge cases', () => { + it('should handle zero rate (should not happen but test defensively)', async () => { + const limiter = new RateLimiter(0) + const mockFn = jest.fn().mockResolvedValue('result') + + // This will hang forever, so we use Promise.race with timeout + const timeoutPromise = new Promise((resolve) => { + setTimeout(() => { + resolve('timeout') + }, 100) + }) + const result = await Promise.race([limiter.exec(mockFn), timeoutPromise]) + + expect(result).toBe('timeout') + }) + + it('should handle very long running operations', async () => { + const limiter = new RateLimiter(1) + const mockFn = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 50)) + return 'result' + }) + + const start = Date.now() + await limiter.exec(mockFn) + const duration = Date.now() - start + + expect(duration).toBeGreaterThanOrEqual(50) + }) + + it('should handle mixed sync and async operations', async () => { + const limiter = new RateLimiter(2) + const syncOp = jest.fn().mockResolvedValue('sync') + const asyncOp = jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return 'async' + }) + + const results = await Promise.all([limiter.exec(syncOp), limiter.exec(asyncOp)]) + + expect(results).toContain('sync') + expect(results).toContain('async') + }) + + it('should handle operations that throw synchronously', async () => { + const limiter = new RateLimiter(1) + const mockFn = jest.fn().mockImplementation(() => { + throw new Error('Sync error') + }) + + await expect(limiter.exec(mockFn)).rejects.toThrow('Sync error') + expect(limiter.processingQueue.size).toBe(0) + }) + + it('should handle notify array correctly when multiple operations wait', async () => { + const limiter = new RateLimiter(1) + const order: number[] = [] + + const createOp = (id: number) => async () => { + order.push(id) + await new Promise((resolve) => setTimeout(resolve, 10)) + return id + } + + // Start multiple operations that will need to wait + const operations = [ + limiter.exec(createOp(1)), + limiter.exec(createOp(2)), + limiter.exec(createOp(3)), + limiter.exec(createOp(4)) + ] + + await Promise.all(operations) + + expect(order).toEqual([1, 2, 3, 4]) + expect(limiter.notify.length).toBe(0) + }) + }) + + describe('concurrent stress test', () => { + it('should handle many concurrent operations correctly', async () => { + const limiter = new RateLimiter(5) + const mockFn = jest.fn().mockImplementation(async (args?: any) => { + await new Promise((resolve) => setTimeout(resolve, Math.random() * 10)) + return args?.id + }) + + const operations = Array(50) + .fill(0) + .map((_, i) => limiter.exec(mockFn, { id: i })) + + const results = await Promise.all(operations) + + expect(mockFn).toHaveBeenCalledTimes(50) + expect(results).toHaveLength(50) + expect(limiter.processingQueue.size).toBe(0) + }) + + it('should maintain rate limit under heavy load', async () => { + const limiter = new RateLimiter(3) + let currentCount = 0 + let maxConcurrent = 0 + + const mockFn = jest.fn().mockImplementation(async () => { + currentCount++ + maxConcurrent = Math.max(maxConcurrent, currentCount) + await new Promise((resolve) => setTimeout(resolve, 10)) + currentCount-- + }) + + const operations = Array(20) + .fill(0) + .map(() => limiter.exec(mockFn)) + + await Promise.all(operations) + + expect(maxConcurrent).toBeLessThanOrEqual(3) + expect(currentCount).toBe(0) + }) + }) +}) diff --git a/foundations/core/packages/core/src/__tests__/test.json b/foundations/core/packages/core/src/__tests__/test.json new file mode 100644 index 0000000000..fc22aa8e01 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/test.json @@ -0,0 +1,126 @@ +[ + { + "_class": "core:class:Class", + "_id": "core:class:Obj", + "createdBy": "core:account:System", + "createdOn": 1729970852414, + "kind": 0, + "label": "Obj", + "modifiedBy": "core:account:System", + "modifiedOn": 1729970852414, + "space": "core:space:Model" + }, + { + "_class": "core:class:Class", + "_id": "core:class:Doc", + "createdBy": "core:account:System", + "createdOn": 1729970852414, + "extends": "core:class:Obj", + "kind": 0, + "label": "Doc", + "modifiedBy": "core:account:System", + "modifiedOn": 1729970852414, + "space": "core:space:Model" + }, + { + "_class": "core:class:Class", + "_id": "core:class:AttachedDoc", + "createdBy": "core:account:System", + "createdOn": 1729970852414, + "extends": "core:class:Doc", + "kind": 2, + "label": "AttachedDoc", + "modifiedBy": "core:account:System", + "modifiedOn": 1729970852414, + "space": "core:space:Model" + }, + { + "_class": "core:class:Class", + "_id": "core:class:Class", + "createdBy": "core:account:System", + "createdOn": 1729970852414, + "domain": "model", + "extends": "core:class:Doc", + "kind": 0, + "label": "Class", + "modifiedBy": "core:account:System", + "modifiedOn": 1729970852414, + "space": "core:space:Model" + }, + { + "_class": "core:class:Class", + "_id": "core:class:Interface", + "createdBy": "core:account:System", + "createdOn": 1729970852414, + "extends": "core:class:Doc", + "kind": 0, + "label": "Interface", + "modifiedBy": "core:account:System", + "modifiedOn": 1729970852414, + "space": "core:space:Model" + }, + { + "_class": "core:class:Class", + "_id": "core:class:Space", + "createdBy": "core:account:System", + "createdOn": 1729970852414, + "domain": "model", + "extends": "core:class:Doc", + "kind": 0, + "label": "Space", + "modifiedBy": "core:account:System", + "modifiedOn": 1729970852414, + "space": "core:space:Model" + }, + { + "_class": "core:class:Class", + "_id": "core:class:Account", + "createdBy": "core:account:System", + "createdOn": 1729970852414, + "domain": "model", + "extends": "core:class:Doc", + "kind": 0, + "label": "Account", + "modifiedBy": "core:account:System", + "modifiedOn": 1729970852414, + "space": "core:space:Model" + }, + { + "_class": "core:class:Interface", + "_id": "test:interface:WithState", + "createdBy": "core:account:System", + "createdOn": 1729970852414, + "extends": [], + "kind": 1, + "label": "WithState", + "modifiedBy": "core:account:System", + "modifiedOn": 1729970852414, + "space": "core:space:Model" + }, + { + "_class": "core:class:Class", + "_id": "core:class:Tx", + "createdBy": "core:account:System", + "createdOn": 1729970852414, + "domain": "tx", + "extends": "core:class:Doc", + "kind": 0, + "label": "Tx", + "modifiedBy": "core:account:System", + "modifiedOn": 1729970852414, + "space": "core:space:Model" + }, + { + "_class": "core:class:Class", + "_id": "core:class:TxCUD", + "createdBy": "core:account:System", + "createdOn": 1729970852414, + "domain": "tx", + "extends": "core:class:Tx", + "kind": 0, + "label": "TxCUD", + "modifiedBy": "core:account:System", + "modifiedOn": 1729970852414, + "space": "core:space:Model" + } +] diff --git a/foundations/core/packages/core/src/__tests__/time.test.ts b/foundations/core/packages/core/src/__tests__/time.test.ts new file mode 100644 index 0000000000..f2d756cd13 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/time.test.ts @@ -0,0 +1,398 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Timestamp } from '../classes' +import { getDay, convertToDay } from '..' + +const supportedTimezones: string[] = [ + 'Europe/Andorra', + 'Asia/Dubai', + 'Asia/Kabul', + 'Europe/Tirane', + 'Asia/Yerevan', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/DumontDUrville', + 'Antarctica/Mawson', + 'Antarctica/Palmer', + 'Antarctica/Rothera', + 'Antarctica/Syowa', + 'Antarctica/Troll', + 'Antarctica/Vostok', + 'America/Argentina/Buenos_Aires', + 'America/Argentina/Cordoba', + 'America/Argentina/Salta', + 'America/Argentina/Jujuy', + 'America/Argentina/Tucuman', + 'America/Argentina/Catamarca', + 'America/Argentina/La_Rioja', + 'America/Argentina/San_Juan', + 'America/Argentina/Mendoza', + 'America/Argentina/San_Luis', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Ushuaia', + 'Pacific/Pago_Pago', + 'Europe/Vienna', + 'Australia/Lord_Howe', + 'Antarctica/Macquarie', + 'Australia/Hobart', + 'Australia/Currie', + 'Australia/Melbourne', + 'Australia/Sydney', + 'Australia/Broken_Hill', + 'Australia/Brisbane', + 'Australia/Lindeman', + 'Australia/Adelaide', + 'Australia/Darwin', + 'Australia/Perth', + 'Australia/Eucla', + 'Asia/Baku', + 'America/Barbados', + 'Asia/Dhaka', + 'Europe/Brussels', + 'Europe/Sofia', + 'Atlantic/Bermuda', + 'Asia/Brunei', + 'America/La_Paz', + 'America/Noronha', + 'America/Belem', + 'America/Fortaleza', + 'America/Recife', + 'America/Araguaina', + 'America/Maceio', + 'America/Bahia', + 'America/Sao_Paulo', + 'America/Campo_Grande', + 'America/Cuiaba', + 'America/Santarem', + 'America/Porto_Velho', + 'America/Boa_Vista', + 'America/Manaus', + 'America/Eirunepe', + 'America/Rio_Branco', + 'America/Nassau', + 'Asia/Thimphu', + 'Europe/Minsk', + 'America/Belize', + 'America/St_Johns', + 'America/Halifax', + 'America/Glace_Bay', + 'America/Moncton', + 'America/Goose_Bay', + 'America/Blanc-Sablon', + 'America/Toronto', + 'America/Nipigon', + 'America/Thunder_Bay', + 'America/Iqaluit', + 'America/Pangnirtung', + 'America/Atikokan', + 'America/Winnipeg', + 'America/Rainy_River', + 'America/Resolute', + 'America/Rankin_Inlet', + 'America/Regina', + 'America/Swift_Current', + 'America/Edmonton', + 'America/Cambridge_Bay', + 'America/Yellowknife', + 'America/Inuvik', + 'America/Creston', + 'America/Dawson_Creek', + 'America/Fort_Nelson', + 'America/Vancouver', + 'America/Whitehorse', + 'America/Dawson', + 'Indian/Cocos', + 'Europe/Zurich', + 'Africa/Abidjan', + 'Pacific/Rarotonga', + 'America/Santiago', + 'America/Punta_Arenas', + 'Pacific/Easter', + 'Asia/Shanghai', + 'Asia/Urumqi', + 'America/Bogota', + 'America/Costa_Rica', + 'America/Havana', + 'Atlantic/Cape_Verde', + 'America/Curacao', + 'Indian/Christmas', + 'Asia/Nicosia', + 'Asia/Famagusta', + 'Europe/Prague', + 'Europe/Berlin', + 'Europe/Copenhagen', + 'America/Santo_Domingo', + 'Africa/Algiers', + 'America/Guayaquil', + 'Pacific/Galapagos', + 'Europe/Tallinn', + 'Africa/Cairo', + 'Africa/El_Aaiun', + 'Europe/Madrid', + 'Africa/Ceuta', + 'Atlantic/Canary', + 'Europe/Helsinki', + 'Atlantic/Stanley', + 'Pacific/Chuuk', + 'Pacific/Pohnpei', + 'Atlantic/Faroe', + 'Europe/Paris', + 'Europe/London', + 'Asia/Tbilisi', + 'America/Cayenne', + 'Africa/Accra', + 'Europe/Gibraltar', + 'America/Godthab', + 'America/Danmarkshavn', + 'America/Scoresbysund', + 'America/Thule', + 'Europe/Athens', + 'Atlantic/South_Georgia', + 'America/Guatemala', + 'Pacific/Guam', + 'Africa/Bissau', + 'America/Guyana', + 'Asia/Hong_Kong', + 'America/Tegucigalpa', + 'America/Port-au-Prince', + 'Europe/Budapest', + 'Asia/Jakarta', + 'Asia/Pontianak', + 'Asia/Makassar', + 'Asia/Jayapura', + 'Europe/Dublin', + 'Asia/Jerusalem', + 'Asia/Kolkata', + 'Indian/Chagos', + 'Asia/Baghdad', + 'Asia/Tehran', + 'Atlantic/Reykjavik', + 'Europe/Rome', + 'America/Jamaica', + 'Asia/Amman', + 'Asia/Tokyo', + 'Africa/Nairobi', + 'Asia/Bishkek', + 'Asia/Pyongyang', + 'Asia/Seoul', + 'Asia/Almaty', + 'Asia/Qyzylorda', + 'Asia/Qostanay', + 'Asia/Aqtobe', + 'Asia/Aqtau', + 'Asia/Atyrau', + 'Asia/Oral', + 'Asia/Beirut', + 'Asia/Colombo', + 'Africa/Monrovia', + 'Europe/Vilnius', + 'Europe/Luxembourg', + 'Europe/Riga', + 'Africa/Tripoli', + 'Africa/Casablanca', + 'Europe/Monaco', + 'Europe/Chisinau', + 'Asia/Yangon', + 'Asia/Ulaanbaatar', + 'Asia/Hovd', + 'Asia/Choibalsan', + 'Asia/Macau', + 'America/Martinique', + 'Europe/Malta', + 'Indian/Mauritius', + 'Indian/Maldives', + 'America/Mexico_City', + 'America/Cancun', + 'America/Merida', + 'America/Monterrey', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Chihuahua', + 'America/Ojinaga', + 'America/Hermosillo', + 'America/Tijuana', + 'America/Bahia_Banderas', + 'Asia/Kuala_Lumpur', + 'Asia/Kuching', + 'Africa/Maputo', + 'Africa/Windhoek', + 'Pacific/Noumea', + 'Africa/Lagos', + 'America/Managua', + 'Europe/Amsterdam', + 'Europe/Oslo', + 'Asia/Kathmandu', + 'Pacific/Niue', + 'America/Panama', + 'America/Lima', + 'Pacific/Tahiti', + 'Pacific/Marquesas', + 'Pacific/Gambier', + 'Pacific/Port_Moresby', + 'Pacific/Bougainville', + 'Asia/Manila', + 'Asia/Karachi', + 'Europe/Warsaw', + 'America/Miquelon', + 'Pacific/Pitcairn', + 'America/Puerto_Rico', + 'Asia/Gaza', + 'Asia/Hebron', + 'Europe/Lisbon', + 'Atlantic/Madeira', + 'Atlantic/Azores', + 'Pacific/Palau', + 'America/Asuncion', + 'Asia/Qatar', + 'Indian/Reunion', + 'Europe/Bucharest', + 'Europe/Belgrade', + 'Europe/Kaliningrad', + 'Europe/Moscow', + 'Europe/Simferopol', + 'Europe/Kirov', + 'Europe/Astrakhan', + 'Europe/Volgograd', + 'Europe/Saratov', + 'Europe/Ulyanovsk', + 'Europe/Samara', + 'Asia/Yekaterinburg', + 'Asia/Omsk', + 'Asia/Novosibirsk', + 'Asia/Barnaul', + 'Asia/Tomsk', + 'Asia/Novokuznetsk', + 'Asia/Krasnoyarsk', + 'Asia/Irkutsk', + 'Asia/Chita', + 'Asia/Yakutsk', + 'Asia/Khandyga', + 'Asia/Vladivostok', + 'Asia/Ust-Nera', + 'Asia/Magadan', + 'Asia/Sakhalin', + 'Asia/Srednekolymsk', + 'Asia/Riyadh', + 'Pacific/Guadalcanal', + 'Indian/Mahe', + 'Africa/Khartoum', + 'Europe/Stockholm', + 'Asia/Singapore', + 'America/Paramaribo', + 'Africa/Juba', + 'Africa/Sao_Tome', + 'America/El_Salvador', + 'Asia/Damascus', + 'America/Grand_Turk', + 'Africa/Ndjamena', + 'Indian/Kerguelen', + 'Asia/Bangkok', + 'Asia/Dushanbe', + 'Asia/Dili', + 'Asia/Ashgabat', + 'Africa/Tunis', + 'Europe/Istanbul', + 'America/Port_of_Spain', + 'Asia/Taipei', + 'Europe/Kiev', + 'Europe/Uzhgorod', + 'Europe/Zaporozhye', + 'America/New_York', + 'America/Detroit', + 'America/Kentucky/Louisville', + 'America/Kentucky/Monticello', + 'America/Indiana/Indianapolis', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Indiana/Marengo', + 'America/Indiana/Petersburg', + 'America/Indiana/Vevay', + 'America/Chicago', + 'America/Indiana/Tell_City', + 'America/Indiana/Knox', + 'America/Menominee', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/North_Dakota/Beulah', + 'America/Denver', + 'America/Boise', + 'America/Phoenix', + 'America/Los_Angeles', + 'America/Anchorage', + 'America/Juneau', + 'America/Sitka', + 'America/Metlakatla', + 'America/Yakutat', + 'America/Nome', + 'America/Adak', + 'Pacific/Honolulu', + 'America/Montevideo', + 'Asia/Samarkand', + 'Asia/Tashkent', + 'America/Caracas', + 'Asia/Ho_Chi_Minh', + 'Pacific/Efate', + 'Africa/Johannesburg' +] + +function testGetDay (date: Date, timezone: string): void { + const timestamp: Timestamp = getDay(date.getTime()) + const convertedDate: Date = new Date(timestamp) + const originalLocaleDate: string = date.toLocaleDateString('en-US', { timeZone: 'Europe/London' }) + const convertedLocaleDate: string = convertedDate.toLocaleDateString('en-US', { timeZone: timezone }) + expect(convertedLocaleDate).toEqual(originalLocaleDate) +} + +function testConvertToDay (date: Date, timezone: string): void { + const convertedDate: Date = convertToDay(date) + const originalLocaleDate: string = date.toLocaleDateString('en-US', { timeZone: 'Europe/London' }) + const convertedLocaleDate: string = convertedDate.toLocaleDateString('en-US', { timeZone: timezone }) + expect(convertedLocaleDate).toEqual(originalLocaleDate) +} + +describe('time', () => { + it.each(supportedTimezones)('dates are matched in `getDay` for locale %p', (timezone: string) => { + testGetDay(new Date(Date.UTC(1995, 11, 3, 12, 0)), timezone) + testGetDay(new Date(Date.UTC(2025, 0, 23, 55, 0)), timezone) + testGetDay(new Date(Date.UTC(2020, 4, 12, 30, 50)), timezone) + testGetDay(new Date(Date.UTC(2024, 5, 12, 30, 50)), timezone) + testGetDay(new Date(Date.UTC(2024, 7, 0, 0, 0)), timezone) + }) + + it.each(supportedTimezones)('dates are matched in `convertToDay` for locale %p', (timezone: string) => { + testConvertToDay(new Date(Date.UTC(1995, 11, 3, 12, 0)), timezone) + testConvertToDay(new Date(Date.UTC(2025, 0, 23, 55, 0)), timezone) + testConvertToDay(new Date(Date.UTC(2020, 4, 12, 30, 50)), timezone) + testConvertToDay(new Date(Date.UTC(2024, 5, 12, 30, 50)), timezone) + testConvertToDay(new Date(Date.UTC(2024, 7, 0, 0, 0)), timezone) + }) + + it.each([ + [0, 0, 0, 0], + [7, 59, 59, 999], + [12, 0, 0, 0], + [23, 59, 59, 999] + ])( + 'dates are matched for time [h: %p, m: %p, s: %p, ms: %p]', + (hours: number, minutes: number, seconds: number, milliSeconds: number) => { + const date: Date = new Date() + const expectedDay: number = date.getDate() + date.setHours(hours, minutes, seconds, milliSeconds) + const convertedDate: Date = convertToDay(date) + expect(convertedDate.getDate()).toEqual(expectedDay) + } + ) +}) diff --git a/foundations/core/packages/core/src/__tests__/utils.test.ts b/foundations/core/packages/core/src/__tests__/utils.test.ts new file mode 100644 index 0000000000..0171bba093 --- /dev/null +++ b/foundations/core/packages/core/src/__tests__/utils.test.ts @@ -0,0 +1,185 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { mergeQueries } from '..' + +describe('mergeQueries', () => { + it('merges query with empty query', () => { + const q1 = { name: 'john', age: { $gt: 42 } } + const q2 = {} + const res = { name: 'john', age: { $gt: 42 } } + + expect(mergeQueries(q1, q2)).toEqual(res) + expect(mergeQueries(q2, q1)).toEqual(res) + }) + + it('merges query with different fields', () => { + const q1 = { name: 'john' } + const q2 = { age: { $gt: 42 } } + const res = { name: 'john', age: { $gt: 42 } } + + expect(mergeQueries(q1, q2)).toEqual(res) + expect(mergeQueries(q2, q1)).toEqual(res) + }) + + it('merges equal field values', () => { + expect(mergeQueries({ value: 42 }, { value: 42 })).toEqual({ value: 42 }) + }) + + it('does not merge different field values', () => { + const q1 = { value: 42 } + const q2 = { value: 'true' } + const res = { value: { $in: [] } } + expect(mergeQueries(q1, q2)).toEqual(res) + expect(mergeQueries(q2, q1)).toEqual(res) + }) + + it('merges predicate with predicate', () => { + expect(mergeQueries({ age: { $in: [41, 42] } }, { age: { $in: [42, 43] } })).toEqual({ age: { $in: [42] } }) + expect(mergeQueries({ age: { $in: [42, 43] } }, { age: { $in: [41, 42] } })).toEqual({ age: { $in: [42] } }) + + expect(mergeQueries({ age: { $nin: [42] } }, { age: { $nin: [42] } })).toEqual({ age: { $nin: [42] } }) + + expect(mergeQueries({ age: { $lt: 45 } }, { age: { $lt: 42 } })).toEqual({ age: { $lt: 42 } }) + expect(mergeQueries({ age: { $lt: 42 } }, { age: { $lt: 45 } })).toEqual({ age: { $lt: 42 } }) + + expect(mergeQueries({ age: { $gt: 40 } }, { age: { $gt: 42 } })).toEqual({ age: { $gt: 42 } }) + expect(mergeQueries({ age: { $gt: 42 } }, { age: { $gt: 40 } })).toEqual({ age: { $gt: 42 } }) + + expect(mergeQueries({ age: { $lte: 45 } }, { age: { $lte: 42 } })).toEqual({ age: { $lte: 42 } }) + expect(mergeQueries({ age: { $lte: 42 } }, { age: { $lte: 45 } })).toEqual({ age: { $lte: 42 } }) + + expect(mergeQueries({ age: { $gte: 40 } }, { age: { $gte: 42 } })).toEqual({ age: { $gte: 42 } }) + expect(mergeQueries({ age: { $gte: 42 } }, { age: { $gte: 40 } })).toEqual({ age: { $gte: 42 } }) + + expect(mergeQueries({ age: { $ne: 42 } }, { age: { $ne: 42 } })).toEqual({ age: { $ne: 42 } }) + }) + + it('merges predicate with value', () => { + // positive + expect(mergeQueries({ age: { $in: [41, 42, 43] } }, { age: 42 })).toEqual({ age: 42 }) + expect(mergeQueries({ age: 42 }, { age: { $in: [41, 42, 43] } })).toEqual({ age: 42 }) + + expect(mergeQueries({ age: { $nin: [41, 43] } }, { age: 42 })).toEqual({ age: 42 }) + expect(mergeQueries({ age: 42 }, { age: { $nin: [41, 43] } })).toEqual({ age: 42 }) + + expect(mergeQueries({ age: { $lt: 45 } }, { age: 42 })).toEqual({ age: 42 }) + expect(mergeQueries({ age: 42 }, { age: { $lt: 45 } })).toEqual({ age: 42 }) + + expect(mergeQueries({ age: { $gt: 40 } }, { age: 42 })).toEqual({ age: 42 }) + expect(mergeQueries({ age: 42 }, { age: { $gt: 40 } })).toEqual({ age: 42 }) + + expect(mergeQueries({ age: { $lte: 42 } }, { age: 42 })).toEqual({ age: 42 }) + expect(mergeQueries({ age: 42 }, { age: { $lte: 42 } })).toEqual({ age: 42 }) + + expect(mergeQueries({ age: { $gte: 42 } }, { age: 42 })).toEqual({ age: 42 }) + expect(mergeQueries({ age: 42 }, { age: { $gte: 42 } })).toEqual({ age: 42 }) + + expect(mergeQueries({ age: { $ne: 40 } }, { age: 42 })).toEqual({ age: 42 }) + expect(mergeQueries({ age: 42 }, { age: { $ne: 40 } })).toEqual({ age: 42 }) + + // negative + expect(mergeQueries({ age: { $in: [31, 32, 33] } }, { age: 42 })).toEqual({ age: { $in: [] } }) + expect(mergeQueries({ age: 42 }, { age: { $in: [31, 32, 33] } })).toEqual({ age: { $in: [] } }) + + expect(mergeQueries({ age: { $nin: [41, 42, 43] } }, { age: 42 })).toEqual({ age: { $in: [] } }) + expect(mergeQueries({ age: 42 }, { age: { $nin: [41, 42, 43] } })).toEqual({ age: { $in: [] } }) + + expect(mergeQueries({ age: { $lt: 42 } }, { age: 42 })).toEqual({ age: { $in: [] } }) + expect(mergeQueries({ age: 42 }, { age: { $lt: 42 } })).toEqual({ age: { $in: [] } }) + + expect(mergeQueries({ age: { $gt: 42 } }, { age: 42 })).toEqual({ age: { $in: [] } }) + expect(mergeQueries({ age: 42 }, { age: { $gt: 42 } })).toEqual({ age: { $in: [] } }) + + expect(mergeQueries({ age: { $lte: 40 } }, { age: 42 })).toEqual({ age: { $in: [] } }) + expect(mergeQueries({ age: 42 }, { age: { $lte: 40 } })).toEqual({ age: { $in: [] } }) + + expect(mergeQueries({ age: { $gte: 43 } }, { age: 42 })).toEqual({ age: { $in: [] } }) + expect(mergeQueries({ age: 42 }, { age: { $gte: 43 } })).toEqual({ age: { $in: [] } }) + + expect(mergeQueries({ age: { $ne: 42 } }, { age: 42 })).toEqual({ age: { $in: [] } }) + expect(mergeQueries({ age: 42 }, { age: { $ne: 42 } })).toEqual({ age: { $in: [] } }) + }) + + it('$in merge', () => { + expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $in: [2, 3] } })).toEqual({ value: { $in: [2] } }) + expect(mergeQueries({ value: { $in: [2, 3] } }, { value: { $in: [1, 2] } })).toEqual({ value: { $in: [2] } }) + + expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $in: [3, 4] } })).toEqual({ value: { $in: [] } }) + expect(mergeQueries({ value: { $in: [3, 4] } }, { value: { $in: [1, 2] } })).toEqual({ value: { $in: [] } }) + + expect(mergeQueries({ value: { $in: [] } }, { value: { $in: [] } })).toEqual({ value: { $in: [] } }) + expect(mergeQueries({ value: { $in: [42] } }, { value: { $in: [42] } })).toEqual({ value: { $in: [42] } }) + }) + + it('$nin merge', () => { + expect(mergeQueries({ value: { $nin: [] } }, { value: { $nin: [] } })).toEqual({}) + expect(mergeQueries({ value: { $nin: [42] } }, { value: { $nin: [42] } })).toEqual({ value: { $nin: [42] } }) + }) + + it('$in $nin $ne merge', () => { + // $in and $nin + expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $nin: [1] } })).toEqual({ value: { $in: [2] } }) + expect(mergeQueries({ value: { $nin: [1] } }, { value: { $in: [1, 2] } })).toEqual({ value: { $in: [2] } }) + + expect(mergeQueries({ value: { $in: ['a', 'b'] } }, { value: { $nin: ['a'] } })).toEqual({ value: { $in: ['b'] } }) + expect(mergeQueries({ value: { $nin: ['a'] } }, { value: { $in: ['a', 'b'] } })).toEqual({ value: { $in: ['b'] } }) + + expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $nin: [1, 2, 3] } })).toEqual({ value: { $in: [] } }) + expect(mergeQueries({ value: { $nin: [1, 2, 3] } }, { value: { $in: [1, 2] } })).toEqual({ value: { $in: [] } }) + + expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $nin: [1, 2] } })).toEqual({ value: { $in: [] } }) + + expect(mergeQueries({ value: { $in: [] } }, { value: { $nin: [42] } })).toEqual({ value: { $in: [] } }) + expect(mergeQueries({ value: { $nin: [42] } }, { value: { $in: [] } })).toEqual({ value: { $in: [] } }) + + // $in and $ne + expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $ne: 1 } })).toEqual({ value: { $in: [2] } }) + expect(mergeQueries({ value: { $ne: 1 } }, { value: { $in: [1, 2] } })).toEqual({ value: { $in: [2] } }) + + expect(mergeQueries({ value: { $in: [1] } }, { value: { $ne: 1 } })).toEqual({ value: { $in: [] } }) + expect(mergeQueries({ value: { $ne: 1 } }, { value: { $in: [1] } })).toEqual({ value: { $in: [] } }) + + expect(mergeQueries({ value: { $in: [] } }, { value: { $ne: 42 } })).toEqual({ value: { $in: [] } }) + expect(mergeQueries({ value: { $ne: 42 } }, { value: { $in: [] } })).toEqual({ value: { $in: [] } }) + }) + + it('$lt and $gt', () => { + expect(mergeQueries({ age: { $lt: 25 } }, { age: { $gt: 20 } })).toEqual({ age: { $lt: 25, $gt: 20 } }) + expect(mergeQueries({ age: { $gt: 20 } }, { age: { $lt: 25 } })).toEqual({ age: { $lt: 25, $gt: 20 } }) + + expect(mergeQueries({ age: { $lt: 20 } }, { age: { $gt: 25 } })).toEqual({ age: { $lt: 20, $gt: 25 } }) + expect(mergeQueries({ age: { $gt: 25 } }, { age: { $lt: 20 } })).toEqual({ age: { $lt: 20, $gt: 25 } }) + }) + + it('complex', () => { + const q1 = { + space: 1, + unique: 'item', + age: { $gt: 10 } + } as any + const q2 = { + space: { $in: [1, 2] }, + age: 30 + } as any + const res = { + space: 1, + unique: 'item', + age: 30 + } as any + expect(mergeQueries(q1, q2)).toEqual(res) + expect(mergeQueries(q2, q1)).toEqual(res) + }) +}) diff --git a/foundations/core/packages/core/src/backup.ts b/foundations/core/packages/core/src/backup.ts new file mode 100644 index 0000000000..348a6606d3 --- /dev/null +++ b/foundations/core/packages/core/src/backup.ts @@ -0,0 +1,32 @@ +import { type Doc, type Domain, type Ref } from './classes' +import { type DocInfo } from './server' + +/** + * @public + * Define a st of document + hashcode for chunk + * So backup client could decide to download or not any of documents. + */ +export interface DocChunk { + idx: number + // _id => hash mapping + docs: DocInfo[] + + size?: number // Estimated size of the chunk data + finished: boolean +} + +/** + * @public + */ +export interface BackupClient { + loadChunk: (domain: Domain, idx?: number) => Promise + closeChunk: (idx: number) => Promise + + loadDocs: (domain: Domain, docs: Ref[]) => Promise + upload: (domain: Domain, docs: Doc[]) => Promise + clean: (domain: Domain, docs: Ref[]) => Promise + + getDomainHash: (domain: Domain) => Promise + + sendForceClose: () => Promise +} diff --git a/foundations/core/packages/core/src/benchmark.ts b/foundations/core/packages/core/src/benchmark.ts new file mode 100644 index 0000000000..7ddd61f0d9 --- /dev/null +++ b/foundations/core/packages/core/src/benchmark.ts @@ -0,0 +1,29 @@ +import { type Doc, type Domain } from './classes' + +/** + * @public + */ +export const DOMAIN_BENCHMARK = 'benchmark' as Domain + +export type BenchmarkDocRange = + | number + | { + // Or random in range + from: number + to: number + } +export interface BenchmarkDoc extends Doc { + source?: string + // Query fields to perform different set of workload + request?: { + // On response will return a set of BenchmarkDoc with requested fields. + documents: BenchmarkDocRange + + // A random sized document with size from to sizeTo + size: BenchmarkDocRange + + // Produce a set of derived documents payload + derived?: BenchmarkDocRange + } + response?: string // A dummy random data to match document's size +} diff --git a/foundations/core/packages/core/src/classes.ts b/foundations/core/packages/core/src/classes.ts new file mode 100644 index 0000000000..85c351eda4 --- /dev/null +++ b/foundations/core/packages/core/src/classes.ts @@ -0,0 +1,948 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021, 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Asset, IntlString, Plugin } from '@hcengineering/platform' +import type { DocumentQuery } from './storage' +import { type WorkspaceDataId, type WorkspaceUuid } from './utils' +import { Tx } from '.' + +/** + * @public + */ +export type Ref = string & { __ref: T } + +/** + * @public + */ +export type PrimitiveType = number | string | boolean | undefined | Ref + +/** + * @public + */ +export type Timestamp = number + +/** + * @public + */ +export type Markup = string + +/** + * @public + */ +export type Hyperlink = string + +/** + * @public + */ +export type CollectionSize = T[]['length'] + +/** + * @public + * + * String representation of {@link https://www.npmjs.com/package/lexorank LexoRank} type + */ +export type Rank = string + +/** + * @public + * + * Reference to blob containing snapshot of collaborative doc. + */ +export type MarkupBlobRef = Ref + +/** + * @public + */ +export interface Obj { + _class: Ref> +} + +export interface Account { + uuid: AccountUuid + role: AccountRole + primarySocialId: PersonId + socialIds: PersonId[] + fullSocialIds: SocialId[] +} + +/** + * @public + * Global person UUID. + */ +export type PersonUuid = string & { __personUuid: true } + +/** + * @public + * Global person account UUID. + * The same UUID as PersonUuid but for when account exists. + */ +export type AccountUuid = PersonUuid & { __accountUuid: true } + +/** + * @public + * Generated identifier of a social id linked to a global person. + */ +export type PersonId = string & { __personId: true } + +export interface BasePerson { + name: string + personUuid?: PersonUuid +} + +/** + * @public + */ +export interface Doc extends Obj { + _id: Ref + space: Ref + modifiedOn: Timestamp + modifiedBy: PersonId + createdBy?: PersonId // Marked as optional since it will be filled by platform. + createdOn?: Timestamp // Marked as optional since it will be filled by platform. +} + +/** + * @public + */ +export type PropertyType = any + +/** + * @public + */ +export interface UXObject extends Obj { + label: IntlString + icon?: Asset + color?: number + hidden?: boolean + readonly?: boolean +} + +/** + * @public + */ +export interface Association extends Doc { + classA: Ref> + classB: Ref> + nameA: string + nameB: string + type: '1:1' | '1:N' | 'N:N' +} + +/** + * @public + */ +export interface Relation extends Doc { + docA: Ref + docB: Ref + association: Ref +} + +/** + * @public + */ +export interface AttachedDoc< + Parent extends Doc = Doc, + Collection extends Extract | string = Extract | string, + S extends Space = Space +> extends Doc { + attachedTo: Ref + attachedToClass: Ref> + collection: Collection +} + +/** + * @public + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface Type extends UXObject {} + +/** + * @public + */ +export enum IndexKind { + /** + * Attribute with this index annotation should be added to elastic for search + * Could be added to string or Ref attribute + * TODO: rename properly for better code readability + */ + FullText, + /** + * For attribute with this annotation should be created an index in mongo database + * + * Also mean to include into Elastic search. + */ + Indexed, + + // Same as indexed but for descending + IndexedDsc +} + +/** + * @public + */ +export interface Enum extends Doc { + name: string + enumValues: string[] +} + +/** + * @public + */ +export interface Attribute extends Doc, UXObject { + attributeOf: Ref> + name: string + type: Type + index?: IndexKind + shortLabel?: IntlString + isCustom?: boolean + defaultValue?: any + automationOnly?: boolean + rank?: Rank + + // Extra customization properties + [key: string]: any +} + +/** + * @public + */ +export type AnyAttribute = Attribute> + +/** + * @public + */ +export enum ClassifierKind { + CLASS, + INTERFACE, + MIXIN +} + +/** + * @public + */ +export interface Classifier extends Doc, UXObject { + kind: ClassifierKind +} + +/** + * @public + */ +export type Domain = string & { __domain: true } + +/** + * @public + */ +export type OperationDomain = string & { __domain: true } + +/** + * @public + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface Interface extends Classifier { + extends?: Ref>[] +} + +/** + * @public + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface Class extends Classifier { + extends?: Ref> + implements?: Ref>[] + domain?: Domain + shortLabel?: string + sortingKey?: string + filteringKey?: string + pluralLabel?: IntlString +} + +/** + * @public + * Define a set of plugin to model document bindings. + */ +export interface PluginConfiguration extends Doc { + pluginId: Plugin + transactions: Ref[] + + label: IntlString + icon?: Asset + description?: IntlString + enabled: boolean + + // If set will not allow to disable this configuration + system?: true + + // If set will not be shown in configuration UI or enabled + hidden?: boolean + + // If specified, will show beta/testing label in UI. + beta: boolean + + // If defined, will only remove classes in list. + classFilter?: Ref>[] +} + +/** + * @public + */ +export type Mixin = Class + +// D A T A + +/** + * @public + */ +export type Data = Omit + +/** + * @public + */ +export type AttachedData = Omit + +/** + * @public + */ +export type DocData = T extends AttachedDoc ? AttachedData : Data + +// T Y P E S + +/** + * @public + */ +export enum DateRangeMode { + DATE = 'date', + TIME = 'time', + DATETIME = 'datetime', + TIMEONLY = 'timeonly' +} + +/** + * @public + */ +export interface TypeDate extends Type { + // If not set date mode default + mode: DateRangeMode + // If not set to true, will be false + withShift: boolean +} + +/** + * @public + */ +export interface TypeIdentifier extends Type { + of: Ref +} + +/** + * @public + */ +export interface TypeNumber extends Type { + min?: number + max?: number + digits?: number // Number of digits after comma +} + +/** + * @public + */ +export interface RefTo extends Type>> { + to: Ref> +} + +/** + * @public + */ +export interface Collection extends Type> { + of: Ref> + itemLabel?: IntlString +} + +/** + * @public + */ +export type Arr = T[] + +/** + * @public + */ +export interface ArrOf extends Type { + of: Type +} + +/** + * @public + */ +export interface EnumOf extends Type { + of: Ref +} + +/** + * @public + */ +export interface TypeHyperlink extends Type {} + +/** + * @public + * + * A type for some custom serialized field with a set of editors + */ +export interface TypeAny extends Type { + presenter: AnyComponent + editor?: AnyComponent +} + +/** + * @public + */ +export const DOMAIN_MODEL = 'model' as Domain + +/** + * @public + */ +export const DOMAIN_MODEL_TX = 'model_tx' as Domain + +/** + * @public + */ +export const DOMAIN_SPACE = 'space' as Domain + +/** + * @public + */ +export const DOMAIN_CONFIGURATION = '_configuration' as Domain + +/** + * @public + */ +export const DOMAIN_MIGRATION = '_migrations' as Domain + +/** + * @public + */ +export const DOMAIN_TRANSIENT = 'transient' as Domain + +/** + * @public + */ +export const DOMAIN_RELATION = 'relation' as Domain + +/** + * @public + */ +export const DOMAIN_COLLABORATOR = 'collaborator' as Domain + +/** + * @public + */ +export interface TransientConfiguration extends Class { + // If set will not store transient objects into memdb + broadcastOnly: boolean +} + +/** + * Special domain to access s3 blob data. + * @public + */ +export const DOMAIN_BLOB = 'blob' as Domain + +/** + * @public + */ +export const DOMAIN_SEQUENCE = 'sequence' as Domain + +// S P A C E + +/** + * @public + */ +export interface Space extends Doc { + name: string + description: string + private: boolean + members: AccountUuid[] + archived: boolean + owners?: AccountUuid[] + autoJoin?: boolean +} + +/** + * @public + */ +export interface SystemSpace extends Space {} + +/** + * @public + * + * Space with custom configured type + */ +export interface TypedSpace extends Space { + type: Ref +} + +/** + * @public + * + * Is used to describe "types" for space type + */ +export interface SpaceTypeDescriptor extends Doc { + name: IntlString + description: IntlString + icon: Asset + baseClass: Ref> // Child class of Space for which the space type can be defined + availablePermissions: Ref[] + system?: boolean +} + +/** + * @public + * + * Customisable space type allowing to configure space roles and permissions within them + */ +export interface SpaceType extends Doc { + name: string + shortDescription?: string + descriptor: Ref + members?: AccountUuid[] // this members will be added automatically to new space, also change this fiield will affect existing spaces + autoJoin?: boolean // if true, all new users will be added to space automatically + targetClass: Ref> // A dynamic mixin for Spaces to hold custom attributes and roles assignment of the space type + roles: CollectionSize +} + +/** + * @public + * Role defines permissions for employees assigned to this role within the space + */ +export interface Role extends AttachedDoc { + name: string + permissions: Ref[] +} + +/** + * @public + * Defines assignment of employees to a role within a space + */ +export type RolesAssignment = Record, AccountUuid[] | undefined> + +/** + * @public + * Permission is a basic access control item in the system + */ +export interface Permission extends Doc { + label: IntlString + txClass?: Ref> + forbid?: boolean + objectClass?: Ref> + scope?: 'space' | 'workspace' + txMatch?: DocumentQuery + description?: IntlString + icon?: Asset +} + +/** + * @public + */ +export enum AccountRole { + ReadOnlyGuest = 'READONLYGUEST', + DocGuest = 'DocGuest', + Guest = 'GUEST', + User = 'USER', + Maintainer = 'MAINTAINER', + Owner = 'OWNER', + Admin = 'ADMIN' +} + +/** + * @public + */ +export const roleOrder: Record = { + [AccountRole.ReadOnlyGuest]: 5, + [AccountRole.DocGuest]: 10, + [AccountRole.Guest]: 20, + [AccountRole.User]: 30, + [AccountRole.Maintainer]: 40, + [AccountRole.Owner]: 50, + [AccountRole.Admin]: 100 +} + +export interface TxAccessLevel extends Class { + createAccessLevel?: AccountRole + removeAccessLevel?: AccountRole + updateAccessLevel?: AccountRole + isIdentity?: boolean +} + +/** + * @public + */ +export interface Person { + uuid: PersonUuid + firstName: string + lastName: string +} + +export interface PersonInfo extends BasePerson { + socialIds: SocialId[] +} + +/** + * @public + */ +// TODO: move to contact +export interface UserStatus extends Doc { + online: boolean + user: AccountUuid +} + +/** + * @public + */ +export interface Version extends Doc { + major: number + minor: number + patch: number +} + +/** + * @public + */ +export interface MigrationState extends Doc { + plugin: string + state: string +} + +/** + * @public + */ +export function versionToString (version: Version | Data): string { + return `${version?.major}.${version?.minor}.${version?.patch}` +} + +/** + * @public + */ +export interface Sequence extends Doc { + attachedTo: Ref> + sequence: number +} + +export interface CustomSequence extends Sequence { + prefix: string +} + +/** + * @public + */ +export type BlobMetadata = Record + +/** + * @public + * + * A blob document to manage blob attached documents. + * + * _id: is a platform ID and it created using our regular generateId(), + * and storageId is a provider specified storage id. + */ +export interface Blob extends Doc { + // Provider + provider: string + // A provider specific id + contentType: string + // A etag for blob + etag: string + // Document version if supported by provider + version: string | null + // A document size + size: number +} + +export interface BlobType { + file: Ref + + type: string + + name: string + size: number + + metadata?: BlobMetadata +} + +export type Blobs = Record + +/** + * For every blob will automatically add a lookup. + * + * It extends Blob to allow for $lookup operations work as expected. + */ +export interface BlobLookup extends Blob { + // An URL document could be downloaded from, with ${id} to put blobId into + downloadUrl: string + downloadUrlExpire?: number +} + +/** + * @public + * + * If defined for class, this class will be enabled for embedding search like openai. + */ +export interface FullTextSearchContext extends Doc { + toClass: Ref> + fullTextSummary?: boolean + forceIndex?: boolean +} + +/** + * @public + */ +export interface ConfigurationElement extends Class { + // Title will be presented to owner. + title: IntlString + // Group for grouping. + group: IntlString +} + +/** + * @public + * + * Define configuration value configuration for workspace. + * + * Configuration is accessible only for owners of workspace and under hood services. + */ +export interface Configuration extends Doc { + enabled: boolean +} + +/** + * @public + */ +export type RelatedDocument = Pick + +/** + * @public + */ +export enum IndexOrder { + Ascending = 1, + Descending = -1 +} + +/** + * @public + */ +export type FieldIndex = { + [P in keyof T]?: IndexOrder +} & Record + +export interface FieldIndexConfig { + sparse?: boolean + filter?: Omit, '$search'> + keys: FieldIndex | string +} + +/** + * @public + * + * Mixin for extra indexing fields. + */ +export interface IndexingConfiguration extends Class { + // Define a list of extra index definitions. + indexes: (string | FieldIndexConfig)[] + searchDisabled?: boolean +} + +export interface DomainIndexConfiguration extends Doc { + domain: Domain + disableCollection?: boolean // For some special cases we could decide to disable collection and index creations at all. + + // A set of indexes we need to disable for domain + // Disabled indexes will be removed + disabled?: (FieldIndex | string)[] + + // Additional indexes we could like to enabled + indexes?: (FieldIndexConfig | string)[] + + skip?: string[] +} + +export type WorkspaceMode = + | 'manual-creation' + | 'pending-creation' // -> 'creating' + | 'creating' // -> 'active + | 'upgrading' // -> 'active' + | 'pending-deletion' // -> 'deleting' + | 'deleting' // -> "deleted" + | 'active' + | 'deleted' + | 'archiving-pending-backup' // -> 'cleaning' + | 'archiving-backup' // -> 'archiving-pending-clean' + | 'archiving-pending-clean' // -> 'archiving-clean' + | 'archiving-clean' // -> 'archived' + | 'archived' + | 'migration-pending-backup' // -> 'migration-backup' + | 'migration-backup' // -> 'migration-pending-cleanup' + | 'migration-pending-clean' // -> 'migration-pending-cleaning' + | 'migration-clean' // -> 'pending-restoring' + | 'pending-restore' // -> 'restoring' + | 'restoring' // -> 'active' + +export type WorkspaceUserOperation = 'archive' | 'migrate-to' | 'unarchive' | 'delete' | 'reset-attempts' + +export function isActiveMode (mode?: WorkspaceMode): boolean { + return mode === 'active' +} +export function isDeletingMode (mode: WorkspaceMode): boolean { + return mode === 'pending-deletion' || mode === 'deleting' || mode === 'deleted' +} +export function isArchivingMode (mode?: WorkspaceMode): boolean { + return ( + mode === 'archiving-pending-backup' || + mode === 'archiving-backup' || + mode === 'archiving-pending-clean' || + mode === 'archiving-clean' || + mode === 'archived' + ) +} + +export function isMigrationMode (mode?: WorkspaceMode): boolean { + return ( + mode === 'migration-pending-backup' || + mode === 'migration-backup' || + mode === 'migration-pending-clean' || + mode === 'migration-clean' + ) +} +export function isRestoringMode (mode?: WorkspaceMode): boolean { + return mode === 'restoring' || mode === 'pending-restore' +} + +export function isUpgradingMode (mode?: WorkspaceMode): boolean { + return mode === 'upgrading' +} + +export type WorkspaceUpdateEvent = + | 'ping' + | 'create-started' + | 'create-done' + | 'upgrade-started' + | 'upgrade-done' + | 'restore-started' + | 'restore-done' + | 'progress' + | 'migrate-backup-started' // -> state = 'migration-backup' + | 'migrate-backup-done' // -> state = 'migration-pending-cleaning' + | 'migrate-clean-started' // -> state = 'migration-cleaning' + | 'migrate-clean-done' // -> state = 'pending-restoring' + | 'archiving-backup-started' // -> state = 'archiving' + | 'archiving-backup-done' // -> state = 'archiving-pending-cleaning' + | 'archiving-clean-started' + | 'archiving-clean-done' + | 'archiving-done' + | 'delete-started' + | 'delete-done' + +export interface WorkspaceInfo { + uuid: WorkspaceUuid + dataId?: WorkspaceDataId // Old workspace identifier. E.g. Database name in Mongo, bucket in R2, etc. + name: string + url: string + region?: string + branding?: string + createdOn: number + createdBy?: PersonUuid // Should always be set for NEW workspaces + billingAccount?: PersonUuid // Should always be set for NEW workspaces + allowReadOnlyGuest?: boolean // Should always be set for NEW workspaces + allowGuestSignUp?: boolean // Should always be set for NEW workspaces +} + +export interface BackupStatus { + dataSize: number + blobsSize: number + + backupSize: number + + lastBackup: Timestamp + backups: number +} + +export interface UsageStatus { + usage: Record + startTime: Timestamp + updateTime: Timestamp +} + +export interface WorkspaceInfoWithStatus extends WorkspaceInfo { + isDisabled?: boolean + versionMajor: number + versionMinor: number + versionPatch: number + lastVisit?: number + mode: WorkspaceMode + processingProgress?: number + backupInfo?: BackupStatus + usageInfo?: UsageStatus + processingAttemps: number +} + +export interface WorkspaceMemberInfo { + person: AccountUuid + role: AccountRole +} + +export enum SocialIdType { + EMAIL = 'email', + GITHUB = 'github', + GOOGLE = 'google', + PHONE = 'phone', + OIDC = 'oidc', + HULY = 'huly', + TELEGRAM = 'telegram', + HULY_ASSISTANT = 'huly-assistant' +} + +export interface SocialId { + // generated ID so the actual social ID can be detached from a person w/o losing the ID in the linked database records + _id: PersonId + + // Should never be changed after creation + type: SocialIdType + value: string + key: string // Calculated from type and value. Just for convenience. + + displayValue?: string + verifiedOn?: number + + isDeleted?: boolean // Social ids are soft-deleted so all objects created with them can still be properly displayed. +} + +export interface AccountInfo { + timezone?: string + locale?: string +} + +export type SocialKey = Pick + +export interface ClassCollaborators extends Doc { + attachedTo: Ref> + fields: (keyof T)[] // PersonId | Ref | PersonId[] | Ref[] + provideSecurity?: boolean // If true, will provide security for collaborators +} + +export interface Collaborator extends AttachedDoc { + collaborator: AccountUuid +} + +/** + * @public + */ +export type IntegrationKind = string & { __IntegrationKind: true } diff --git a/foundations/core/packages/core/src/client.ts b/foundations/core/packages/core/src/client.ts new file mode 100644 index 0000000000..2226f60d6f --- /dev/null +++ b/foundations/core/packages/core/src/client.ts @@ -0,0 +1,465 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Analytics } from '@hcengineering/analytics' +import { type BackupClient, type DocChunk } from './backup' +import { + type Class, + DOMAIN_MODEL, + type Doc, + type Domain, + type OperationDomain, + type Ref, + type Timestamp +} from './classes' +import core from './component' +import { Hierarchy } from './hierarchy' +import { type MeasureContext, MeasureMetricsContext } from '@hcengineering/measurements' +import { ModelDb } from './memdb' +import type { + DocumentQuery, + DomainParams, + DomainResult, + FindOptions, + FindResult, + FulltextStorage, + SearchOptions, + SearchQuery, + SearchResult, + Storage, + TxResult, + WithLookup +} from './storage' +import { type Tx, type TxWorkspaceEvent, WorkspaceEvent } from './tx' +import { platformNow, platformNowDiff, toFindResult } from './utils' + +/** + * @public + */ +export type TxHandler = (...tx: Tx[]) => void + +export interface DomainRequestOptions { + retry?: boolean +} +/** + * @public + */ +export interface Client extends Storage, FulltextStorage { + notify?: (...tx: Tx[]) => void + getHierarchy: () => Hierarchy + getModel: () => ModelDb + findOne: ( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ) => Promise | undefined> + close: () => Promise + + domainRequest: ( + domain: OperationDomain, + params: DomainParams, + options?: DomainRequestOptions + ) => Promise> +} + +/** + * @public + */ +export interface LoadModelResponse { + // A diff or a full set of transactions. + transactions: Tx[] + // A current hash chain + hash: string + // If full model is returned, on hash diff for request + full: boolean +} + +/** + * @public + */ +export enum ClientConnectEvent { + Connected, // In case we just connected to server, and receive a full model + Reconnected, // In case we re-connected to server and receive and apply diff. + + // Client could cause back a few more states. + Upgraded, // In case client code receive a full new model and need to be rebuild. + Refresh, // In case we detect query refresh is required + Maintenance // In case workspace are in maintenance mode +} + +/** + * @public + */ +export interface ClientConnection extends Storage, FulltextStorage, BackupClient { + isConnected: () => boolean + + close: () => Promise + onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise + + // If hash is passed, will return LoadModelResponse + loadModel: (last: Timestamp, hash?: string) => Promise + getLastHash?: (ctx: MeasureContext) => Promise + pushHandler: (handler: TxHandler) => void + domainRequest: (ctx: OperationDomain, params: DomainParams, options?: DomainRequestOptions) => Promise +} + +class ClientImpl implements Client, BackupClient { + notify?: (...tx: Tx[]) => void + hierarchy!: Hierarchy + model!: ModelDb + private readonly appliedModelTransactions = new Set>() + constructor (private readonly conn: ClientConnection) {} + + getConnection (): ClientConnection { + return this.conn + } + + setModel (hierarchy: Hierarchy, model: ModelDb): void { + this.hierarchy = hierarchy + this.model = model + } + + getHierarchy (): Hierarchy { + return this.hierarchy + } + + getModel (): ModelDb { + return this.model + } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + const domain = this.hierarchy.getDomain(_class) + const data = + domain === DOMAIN_MODEL + ? await this.model.findAll(_class, query, options) + : await this.conn.findAll(_class, query, options) + + // In case of mixin we need to create mixin proxies. + + // Update mixins & lookups + const result = data.map((v) => { + return this.hierarchy.updateLookupMixin(_class, v, options) + }) + return toFindResult(result, data.total) + } + + async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { + return await this.conn.searchFulltext(query, options) + } + + async domainRequest ( + ctx: OperationDomain, + params: DomainParams, + options?: DomainRequestOptions + ): Promise { + return await this.conn.domainRequest(ctx, params, options) + } + + async findOne( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise | undefined> { + return (await this.findAll(_class, query, { ...options, limit: 1 }))[0] + } + + async tx (tx: Tx): Promise { + if (tx.objectSpace === core.space.Model) { + this.hierarchy.tx(tx) + await this.model.tx(tx) + this.appliedModelTransactions.add(tx._id) + } + // We need to handle it on server, before performing local live query updates. + return await this.conn.tx(tx) + } + + async updateFromRemote (...tx: Tx[]): Promise { + for (const t of tx) { + try { + if (t.objectSpace === core.space.Model) { + const hasTx = this.appliedModelTransactions.has(t._id) + if (!hasTx) { + this.hierarchy.tx(t) + await this.model.tx(t) + } else { + this.appliedModelTransactions.delete(t._id) + } + } + } catch (err) { + // console.error('failed to apply model transaction, skipping', t) + continue + } + } + this.notify?.(...tx) + } + + async close (): Promise { + await this.conn.close() + } + + async loadChunk (domain: Domain, idx?: number): Promise { + return await this.conn.loadChunk(domain, idx) + } + + async getDomainHash (domain: Domain): Promise { + return await this.conn.getDomainHash(domain) + } + + async closeChunk (idx: number): Promise { + await this.conn.closeChunk(idx) + } + + async loadDocs (domain: Domain, docs: Ref[]): Promise { + return await this.conn.loadDocs(domain, docs) + } + + async upload (domain: Domain, docs: Doc[]): Promise { + await this.conn.upload(domain, docs) + } + + async clean (domain: Domain, docs: Ref[]): Promise { + await this.conn.clean(domain, docs) + } + + async sendForceClose (): Promise { + await this.conn.sendForceClose() + } +} + +/** + * @public + */ +export interface TxPersistenceStore { + load: () => Promise + store: (model: LoadModelResponse) => Promise +} + +export type ModelFilter = (tx: Tx[]) => Tx[] + +/** + * @public + */ +export async function createClient ( + connect: (txHandler: TxHandler) => Promise, + // If set will build model with only allowed plugins. + modelFilter?: ModelFilter, + txPersistence?: TxPersistenceStore, + _ctx?: MeasureContext +): Promise { + const ctx = _ctx ?? new MeasureMetricsContext('createClient', {}) + let client: ClientImpl | null = null + + // Temporal buffer, while we apply model + let txBuffer: Tx[] | undefined = [] + + let hierarchy = new Hierarchy() + let model = new ModelDb(hierarchy) + + let lastTx: string | undefined + + function txHandler (...tx: Tx[]): void { + if (tx == null || tx.length === 0) { + return + } + if (client === null) { + txBuffer?.push(...tx) + } else { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + client.updateFromRemote(...tx) + } + for (const t of tx) { + if (t._class === core.class.TxWorkspaceEvent && (t as TxWorkspaceEvent).event === WorkspaceEvent.LastTx) { + lastTx = (t as TxWorkspaceEvent).params.lastTx + } + } + } + const conn = await ctx.with('connect', {}, () => connect(txHandler), {}, { suspendErrors: true }) + + let { mode, current, addition } = await ctx.with('load-model', {}, (ctx) => loadModel(ctx, conn, txPersistence)) + switch (mode) { + case 'same': + case 'upgrade': + ctx.withSync('build-model', {}, (ctx) => { + buildModel(ctx, current, modelFilter, hierarchy, model) + }) + break + case 'addition': + ctx.withSync('build-model', {}, (ctx) => { + buildModel(ctx, current.concat(addition), modelFilter, hierarchy, model) + }) + } + current = [] + addition = [] + + txBuffer = txBuffer.filter((tx) => tx.space !== core.space.Model) + + client = new ClientImpl(conn) + client.setModel(hierarchy, model) + + txHandler(...txBuffer) + txBuffer = undefined + + const oldOnConnect: + | ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) + | undefined = conn.onConnect + conn.onConnect = async (event, _lastTx, data) => { + console.log('Client: onConnect', event) + if (event === ClientConnectEvent.Maintenance) { + lastTx = _lastTx + await oldOnConnect?.(ClientConnectEvent.Maintenance, _lastTx, data) + return + } + // Find all new transactions and apply + let { mode, current, addition } = await ctx.with('load-model', {}, (ctx) => loadModel(ctx, conn, txPersistence)) + + switch (mode) { + case 'upgrade': + // We have upgrade procedure and need rebuild all stuff. + hierarchy = new Hierarchy() + model = new ModelDb(hierarchy) + client.setModel(hierarchy, model) + + ctx.withSync('build-model', {}, (ctx) => { + buildModel(ctx, current, modelFilter, hierarchy, model) + }) + current = [] + await oldOnConnect?.(ClientConnectEvent.Upgraded, _lastTx, data) + // No need to fetch more stuff since upgrade was happened. + break + case 'addition': + ctx.withSync('build-model', {}, (ctx) => { + buildModel(ctx, current.concat(addition), modelFilter, hierarchy, model) + }) + break + } + current = [] + addition = [] + + if (lastTx === undefined) { + // No need to do anything here since we connected. + await oldOnConnect?.(event, _lastTx, data) + lastTx = _lastTx + return + } + + if (lastTx === _lastTx) { + // Same lastTx, no need to refresh + await oldOnConnect?.(ClientConnectEvent.Reconnected, _lastTx, data) + return + } + lastTx = _lastTx + // We need to trigger full refresh on queries, etc. + await oldOnConnect?.(ClientConnectEvent.Refresh, lastTx, data) + } + + return client +} + +async function loadModel ( + ctx: MeasureContext, + conn: ClientConnection, + persistence?: TxPersistenceStore +): Promise<{ mode: 'same' | 'addition' | 'upgrade', current: Tx[], addition: Tx[] }> { + const t = platformNow() + + const current = (await ctx.with('persistence-load', {}, () => persistence?.load())) ?? { + full: true, + transactions: [], + hash: '' + } + + if (conn.getLastHash !== undefined && (await conn.getLastHash(ctx)) === current.hash) { + // We have same model hash. + return { mode: 'same', current: current.transactions, addition: [] } + } + const lastTxTime = getLastTxTime(current.transactions) + const result = await ctx.with('connection-load-model', { hash: current.hash !== '' }, (ctx) => + conn.loadModel(lastTxTime, current.hash) + ) + + if (Array.isArray(result)) { + // Fallback to old behavior, only for tests + return { + mode: 'same', + current: result, + addition: [] + } + } + + // Save concatenated, if have some more of them. + void ctx + .with('persistence-store', {}, (ctx) => + persistence?.store({ + ...result, + // Store concatinated old + new txes + transactions: result.full ? result.transactions : current.transactions.concat(result.transactions) + }) + ) + .catch((err) => { + Analytics.handleError(err) + }) + + if (typeof window !== 'undefined') { + console.log('find' + (result.full ? 'full model' : 'model diff'), result.transactions.length, platformNowDiff(t)) + } + if (result.full) { + return { mode: 'upgrade', current: result.transactions, addition: [] } + } + return { mode: 'addition', current: current.transactions, addition: result.transactions } +} + +export function buildModel ( + ctx: MeasureContext, + transactions: Tx[], + modelFilter: ModelFilter | undefined, + hierarchy: Hierarchy, + model: ModelDb +): void { + let txes = transactions + if (modelFilter !== undefined) { + txes = modelFilter(txes) + } + + ctx.withSync('build hierarchy', {}, () => { + for (const tx of txes) { + try { + hierarchy.tx(tx) + } catch (err: any) { + ctx.warn('failed to apply model transaction, skipping', { + _id: tx._id, + _class: tx._class, + message: err?.message + }) + } + } + }) + ctx.withSync('build model', {}, (ctx) => { + model.addTxes(ctx, txes, false) + }) +} + +function getLastTxTime (txes: Tx[]): number { + let lastTxTime = 0 + for (const tx of txes) { + if (tx.modifiedOn > lastTxTime) { + lastTxTime = tx.modifiedOn + } + } + return lastTxTime +} diff --git a/foundations/core/packages/core/src/clone.ts b/foundations/core/packages/core/src/clone.ts new file mode 100644 index 0000000000..060a7e597b --- /dev/null +++ b/foundations/core/packages/core/src/clone.ts @@ -0,0 +1,84 @@ +const se = typeof Symbol !== 'undefined' +const ste = se && typeof Symbol.toStringTag !== 'undefined' + +export function getTypeOf (obj: any): string { + const typeofObj = typeof obj + if (typeofObj !== 'object') { + return typeofObj + } + if (obj === null) { + return 'null' + } + + if (Array.isArray(obj) && (!ste || !(Symbol.toStringTag in obj))) { + return 'Array' + } + + const stringTag = ste && obj[Symbol.toStringTag] + if (typeof stringTag === 'string') { + return stringTag + } + + const objPrototype = Object.getPrototypeOf(obj) + + if (objPrototype === RegExp.prototype) { + return 'RegExp' + } + if (objPrototype === Date.prototype) { + return 'Date' + } + + if (objPrototype === null) { + return 'Object' + } + return {}.toString.call(obj).slice(8, -1) +} + +export function clone ( + obj: any, + as?: (doc: any, m: any) => any, + needAs?: (value: any) => any | undefined, + depth?: number +): any { + if (typeof obj === 'undefined') { + return undefined + } + if (typeof obj === 'function') { + return obj + } + if (depth === 0) { + return obj + } + depth = depth === undefined ? depth : depth - 1 + const typeOf = getTypeOf(obj) + if (typeOf === 'Date') { + return new Date(obj.getTime()) + } else if (typeOf === 'Array' || typeOf === 'Object') { + const isArray = Array.isArray(obj) + const result: any = isArray ? [] : Object.assign({}, obj) + for (const key in obj) { + // include prototype properties + const value = obj[key] + const type = getTypeOf(value) + if (type === 'Array') { + result[key] = clone(value, as, needAs, depth) + } else if (type === 'Object') { + const valClone = clone(value, as, needAs, depth) + result[key] = valClone + } else if (type === 'Date') { + result[key] = new Date(value.getTime()) + } else { + if (isArray) { + result[key] = value + } + } + } + if (typeOf === 'Object') { + const m = needAs?.(obj) + return m !== undefined && as !== undefined ? as(result, m) : result + } + return result + } else { + return obj + } +} diff --git a/foundations/core/packages/core/src/collaboration.ts b/foundations/core/packages/core/src/collaboration.ts new file mode 100644 index 0000000000..ecf14a96b9 --- /dev/null +++ b/foundations/core/packages/core/src/collaboration.ts @@ -0,0 +1,53 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Blob, Class, Doc, MarkupBlobRef, Ref } from './classes' + +/** @public */ +export interface CollaborativeDoc { + objectClass: Ref> + objectId: Ref + objectAttr: string +} + +/** @public */ +export function makeCollabId ( + objectClass: Ref>, + objectId: Ref, + objectAttr: Extract | string +): CollaborativeDoc { + return { objectClass, objectId, objectAttr } +} + +/** @public */ +export function makeDocCollabId ( + doc: T, + objectAttr: Extract | string +): CollaborativeDoc { + return makeCollabId(doc._class, doc._id, objectAttr) +} + +/** @public */ +export function makeCollabYdocId (doc: CollaborativeDoc): Ref { + const { objectId, objectAttr } = doc + return `${objectId}%${objectAttr}` as Ref +} + +/** @public */ +export function makeCollabJsonId (doc: CollaborativeDoc): MarkupBlobRef { + const timestamp = Date.now() + const { objectId, objectAttr } = doc + return [objectId, objectAttr, timestamp].join('-') as MarkupBlobRef +} diff --git a/foundations/core/packages/core/src/collaborators.ts b/foundations/core/packages/core/src/collaborators.ts new file mode 100644 index 0000000000..1574a2c140 --- /dev/null +++ b/foundations/core/packages/core/src/collaborators.ts @@ -0,0 +1,37 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { Class, ClassCollaborators, Doc, Hierarchy, ModelDb, Ref } from '.' + +export function getClassCollaborators ( + model: ModelDb, + hiearachy: Hierarchy, + _id: Ref> +): ClassCollaborators | undefined { + const ancestors = hiearachy.getAncestors(_id) + const collabs = new Map( + model + .findAllSync(core.class.ClassCollaborators, { + attachedTo: { $in: ancestors } + }) + .map((c) => [c.attachedTo, c]) + ) + for (const ancestor of ancestors) { + const res = collabs.get(ancestor) + if (res !== undefined) { + return res + } + } +} diff --git a/foundations/core/packages/core/src/common.ts b/foundations/core/packages/core/src/common.ts new file mode 100644 index 0000000000..fc4ee3158f --- /dev/null +++ b/foundations/core/packages/core/src/common.ts @@ -0,0 +1,41 @@ +export function groupByArray (array: T[], keyProvider: (item: T) => K): Map { + const result = new Map() + + array.forEach((item) => { + const key = keyProvider(item) + + if (!result.has(key)) { + result.set(key, [item]) + } else { + result.get(key)?.push(item) + } + }) + + return result +} + +export async function groupByArrayAsync (array: T[], keyProvider: (item: T) => Promise): Promise> { + const result = new Map() + + for (const item of array) { + const key = await keyProvider(item) + + if (!result.has(key)) { + result.set(key, [item]) + } else { + result.get(key)?.push(item) + } + } + + return result +} + +export function flipSet (set: Set, item: T): Set { + if (set.has(item)) { + set.delete(item) + } else { + set.add(item) + } + + return set +} diff --git a/foundations/core/packages/core/src/component.ts b/foundations/core/packages/core/src/component.ts new file mode 100644 index 0000000000..441c27ed75 --- /dev/null +++ b/foundations/core/packages/core/src/component.ts @@ -0,0 +1,312 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform' +import { plugin } from '@hcengineering/platform' +import type { BenchmarkDoc } from './benchmark' +import { AccountRole, TxAccessLevel } from './classes' +import type { + Account, + AnyAttribute, + ArrOf, + Association, + AttachedDoc, + Blob, + Class, + Collection, + Configuration, + ConfigurationElement, + Doc, + DomainIndexConfiguration, + Enum, + EnumOf, + FullTextSearchContext, + Hyperlink, + IndexingConfiguration, + Interface, + MarkupBlobRef, + MigrationState, + Mixin, + Obj, + Permission, + PersonId, + PluginConfiguration, + Rank, + Ref, + RefTo, + RelatedDocument, + Relation, + Role, + Sequence, + CustomSequence, + Space, + SpaceType, + SpaceTypeDescriptor, + SystemSpace, + Timestamp, + TransientConfiguration, + Type, + TypeAny, + TypedSpace, + UserStatus, + Version, + AccountUuid, + ClassCollaborators, + Collaborator +} from './classes' +import { type Status, type StatusCategory } from './status' +import type { + Tx, + TxApplyIf, + TxCUD, + TxCreateDoc, + TxDomainEvent, + TxMixin, + TxModelUpgrade, + TxRemoveDoc, + TxUpdateDoc, + TxWorkspaceEvent +} from './tx' + +/** + * @public + */ +export const coreId = 'core' as Plugin + +/** + * @public + */ +// TODO: consider removing email? +export const systemAccountEmail = 'anticrm@hc.engineering' +export const systemAccountUuid = '1749089e-22e6-48de-af4e-165e18fbd2f9' as AccountUuid +export const systemAccount: Account = { + uuid: systemAccountUuid, + role: AccountRole.Owner, + primarySocialId: '' as PersonId, + socialIds: [], + fullSocialIds: [] +} + +export const configUserAccountUuid = '0d94731c-0787-4bcd-aefe-304efc3706b1' as AccountUuid + +export const readOnlyGuestAccountUuid = '83bbed9a-0867-4851-be32-31d49d1d42ce' as AccountUuid + +export default plugin(coreId, { + class: { + Obj: '' as Ref>, + Doc: '' as Ref>, + Blob: '' as Ref>, + AttachedDoc: '' as Ref>, + Class: '' as Ref>>, + Mixin: '' as Ref>>, + Interface: '' as Ref>>, + Attribute: '' as Ref>, + Tx: '' as Ref>, + TxModelUpgrade: '' as Ref>, + TxWorkspaceEvent: '' as Ref>, + TxDomainEvent: '' as Ref>, + TxApplyIf: '' as Ref>, + TxCUD: '' as Ref>>, + TxCreateDoc: '' as Ref>>, + TxMixin: '' as Ref>>, + TxUpdateDoc: '' as Ref>>, + TxRemoveDoc: '' as Ref>>, + Space: '' as Ref>, + SystemSpace: '' as Ref>, + TypedSpace: '' as Ref>, + SpaceTypeDescriptor: '' as Ref>, + SpaceType: '' as Ref>, + Role: '' as Ref>, + Permission: '' as Ref>, + Type: '' as Ref>>, + TypeRelation: '' as Ref>>, + TypeString: '' as Ref>>, + TypeBlob: '' as Ref>>>, + TypeIntlString: '' as Ref>>, + TypeHyperlink: '' as Ref>>, + TypeNumber: '' as Ref>>, + TypeFileSize: '' as Ref>>, + TypeMarkup: '' as Ref>>, + TypeRank: '' as Ref>>, + TypeRecord: '' as Ref>>>, + TypeBoolean: '' as Ref>>, + TypeTimestamp: '' as Ref>>, + TypeDate: '' as Ref>>, + TypeCollaborativeDoc: '' as Ref>>, + TypePersonId: '' as Ref>>, + TypeAccountUuid: '' as Ref>>, + TypeIdentifier: '' as Ref>>, + RefTo: '' as Ref>>, + ArrOf: '' as Ref>>, + Enum: '' as Ref>, + EnumOf: '' as Ref>, + Collection: '' as Ref>>, + TypeAny: '' as Ref>, + Version: '' as Ref>, + PluginConfiguration: '' as Ref>, + UserStatus: '' as Ref>, + + TypeRelatedDocument: '' as Ref>>, + DomainIndexConfiguration: '' as Ref>, + + Configuration: '' as Ref>, + + Status: '' as Ref>, + StatusCategory: '' as Ref>, + MigrationState: '' as Ref>, + + BenchmarkDoc: '' as Ref>, + FullTextSearchContext: '' as Ref>, + Association: '' as Ref>, + Relation: '' as Ref>, + Sequence: '' as Ref>, + CustomSequence: '' as Ref>, + ClassCollaborators: '' as Ref>>, + Collaborator: '' as Ref> + }, + icon: { + TypeString: '' as Asset, + TypeBlob: '' as Asset, + TypeHyperlink: '' as Asset, + TypeNumber: '' as Asset, + TypeMarkup: '' as Asset, + TypeRank: '' as Asset, + TypeRecord: '' as Asset, + TypeBoolean: '' as Asset, + TypeDate: '' as Asset, + TypeRef: '' as Asset, + TypeArray: '' as Asset, + TypeEnumOf: '' as Asset, + TypeCollection: '' as Asset + }, + mixin: { + ConfigurationElement: '' as Ref>, + IndexConfiguration: '' as Ref>>, + SpacesTypeData: '' as Ref>, + TransientConfiguration: '' as Ref>, + TxAccessLevel: '' as Ref> + }, + space: { + Tx: '' as Ref, + DerivedTx: '' as Ref, + Model: '' as Ref, + Space: '' as Ref, + Configuration: '' as Ref, + Workspace: '' as Ref, + Domain: '' as Ref + }, + employee: { + System: '' as Ref // An system employee reference. + }, + account: { + System: '' as PersonId, + ConfigUser: '' as PersonId + }, + status: { + ObjectNotFound: '' as StatusCode<{ _id: Ref }>, + ItemNotFound: '' as StatusCode<{ _id: Ref, _localId: string }> + }, + version: { + Model: '' as Ref + }, + string: { + Id: '' as IntlString, + Space: '' as IntlString, + Spaces: '' as IntlString, + SpacesDescription: '' as IntlString, + TypedSpace: '' as IntlString, + SpaceType: '' as IntlString, + Modified: '' as IntlString, + ModifiedDate: '' as IntlString, + ModifiedBy: '' as IntlString, + Class: '' as IntlString, + AttachedTo: '' as IntlString, + AttachedToClass: '' as IntlString, + String: '' as IntlString, + Record: '' as IntlString, + Markup: '' as IntlString, + Relation: '' as IntlString, + Relations: '' as IntlString, + AddRelation: '' as IntlString, + Collaborative: '' as IntlString, + CollaborativeDoc: '' as IntlString, + MarkupBlobRef: '' as IntlString, + PersonId: '' as IntlString, + AccountId: '' as IntlString, + Number: '' as IntlString, + Boolean: '' as IntlString, + Timestamp: '' as IntlString, + Date: '' as IntlString, + IntlString: '' as IntlString, + Ref: '' as IntlString, + Collection: '' as IntlString, + Array: '' as IntlString, + Name: '' as IntlString, + Enum: '' as IntlString, + Size: '' as IntlString, + Description: '' as IntlString, + ShortDescription: '' as IntlString, + Descriptor: '' as IntlString, + TargetClass: '' as IntlString, + Role: '' as IntlString, + Roles: '' as IntlString, + Hyperlink: '' as IntlString, + Private: '' as IntlString, + Object: '' as IntlString, + System: '' as IntlString, + CreatedBy: '' as IntlString, + CreatedDate: '' as IntlString, + Status: '' as IntlString, + Account: '' as IntlString, + StatusCategory: '' as IntlString, + Rank: '' as IntlString, + Members: '' as IntlString, + Owners: '' as IntlString, + Permission: '' as IntlString, + CreateObject: '' as IntlString, + UpdateObject: '' as IntlString, + DeleteObject: '' as IntlString, + ForbidDeleteObject: '' as IntlString, + UpdateSpace: '' as IntlString, + ArchiveSpace: '' as IntlString, + CreateObjectDescription: '' as IntlString, + UpdateObjectDescription: '' as IntlString, + DeleteObjectDescription: '' as IntlString, + ForbidDeleteObjectDescription: '' as IntlString, + UpdateSpaceDescription: '' as IntlString, + ArchiveSpaceDescription: '' as IntlString, + AutoJoin: '' as IntlString, + AutoJoinDescr: '' as IntlString + }, + descriptor: { + SpacesType: '' as Ref + }, + spaceType: { + SpacesType: '' as Ref + }, + permission: { + CreateObject: '' as Ref, + UpdateObject: '' as Ref, + DeleteObject: '' as Ref, + ForbidDeleteObject: '' as Ref, + UpdateSpace: '' as Ref, + ArchiveSpace: '' as Ref + }, + role: { + Admin: '' as Ref + }, + metadata: { + DisablePermissions: '' as Metadata + } +}) diff --git a/foundations/core/packages/core/src/hierarchy.ts b/foundations/core/packages/core/src/hierarchy.ts new file mode 100644 index 0000000000..9b8fc3665c --- /dev/null +++ b/foundations/core/packages/core/src/hierarchy.ts @@ -0,0 +1,650 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type FindOptions, type Lookup, type ToClassRefT, type WithLookup } from '.' +import type { AnyAttribute, Class, Classifier, Doc, Domain, Interface, Mixin, Obj, Ref } from './classes' +import { ClassifierKind } from './classes' +import { clone as deepClone } from './clone' +import core from './component' +import { _createMixinProxy, _mixinClass, _toDoc, PROXY_MIXIN_CLASS_KEY } from './proxy' +import type { Tx, TxCreateDoc, TxCUD, TxMixin, TxRemoveDoc, TxUpdateDoc } from './tx' +import { TxProcessor } from './tx' + +/** + * @public + */ +export class Hierarchy { + private readonly classifiers = new Map, Classifier>() + private readonly attributes = new Map, Map>() + private readonly attributesById = new Map, AnyAttribute>() + private readonly descendants = new Map, Ref[]>() + private readonly ancestors = new Map, Ref[]>() + private readonly proxies = new Map>, ProxyHandler>() + + private readonly classifierProperties = new Map, Map>() + + private createMixinProxyHandler (mixin: Ref>): ProxyHandler { + const value = this.getClass(mixin) + const ancestor = this.getClass(value.extends as Ref>) + const ancestorProxy = ancestor.kind === ClassifierKind.MIXIN ? this.getMixinProxyHandler(ancestor._id) : null + return _createMixinProxy(value, ancestorProxy) + } + + private getMixinProxyHandler (mixin: Ref>): ProxyHandler { + const handler = this.proxies.get(mixin) + if (handler === undefined) { + const handler = this.createMixinProxyHandler(mixin) + this.proxies.set(mixin, handler) + return handler + } + return handler + } + + as(doc: D, mixin: Ref>): M { + if ((doc as any)[PROXY_MIXIN_CLASS_KEY] === mixin) return doc as M + + return new Proxy(Hierarchy.toDoc(doc), this.getMixinProxyHandler(mixin)) as M + } + + asIf(doc: D | undefined, mixin: Ref>): M | undefined { + if (doc === undefined) { + return undefined + } + return this.hasMixin(doc, mixin) ? this.as(doc, mixin) : undefined + } + + asIfArray(docs: D[], mixin: Ref>): M[] { + return docs.map((it) => this.asIf(it, mixin)).filter((it) => it !== undefined) + } + + static toDoc(doc: D): D { + return _toDoc(doc) + } + + static mixinClass(doc: D): Ref> | undefined { + return _mixinClass(doc) + } + + static mixinOrClass(doc: D): Ref | Class> { + const m = _mixinClass(doc) + return m ?? doc._class + } + + static hasMixin(doc: D, mixin: Ref>): boolean { + const d = Hierarchy.toDoc(doc) + return typeof (d as any)[mixin] === 'object' + } + + hasMixin(doc: D, mixin: Ref>): boolean { + return Hierarchy.hasMixin(doc, mixin) + } + + classHierarchyMixin( + _class: Ref>, + mixin: Ref>, + filter?: (value: M) => boolean + ): M | undefined { + let clazz = this.getClass(_class) + while (true) { + if (this.hasMixin(clazz, mixin)) { + const m = this.as(clazz, mixin) as any as M + if (m !== undefined && (filter?.(m) ?? true)) { + return m + } + } + if (clazz.extends === undefined) return + clazz = this.getClass(clazz.extends) + } + } + + findClassOrMixinMixin(doc: Doc, mixin: Ref>): M | undefined { + const cc = this.classHierarchyMixin(doc._class, mixin) + if (cc !== undefined) { + return cc + } + + const _doc = _toDoc(doc) + // Find all potential mixins of doc + for (const [k, v] of Object.entries(_doc)) { + if (typeof v === 'object' && this.classifiers.has(k as Ref)) { + const cc = this.classHierarchyMixin(k as Ref>, mixin) + if (cc !== undefined) { + return cc + } + } + } + } + + findMixinMixins(doc: Doc, mixin: Ref>): M[] { + const _doc = _toDoc(doc) + const result: M[] = [] + const resultSet = new Set() + // Find all potential mixins of doc + for (const [k, v] of Object.entries(_doc)) { + if (typeof v === 'object' && this.classifiers.has(k as Ref)) { + const clazz = this.getClass(k as Ref) + if (this.hasMixin(clazz, mixin)) { + const cc = this.as(clazz, mixin) as any as M + if (cc !== undefined && !resultSet.has(cc._id)) { + result.push(cc) + resultSet.add(cc._id) + } + } + } + } + return result + } + + findAllMixins(doc: Doc): Ref>[] { + const _doc = _toDoc(doc) + const resultSet = new Set>>() + for (const [k, v] of Object.entries(_doc)) { + if (typeof v === 'object' && this.classifiers.has(k as Ref)) { + if (this.isMixin(k as Ref)) { + if (!resultSet.has(k as Ref)) { + resultSet.add(k as Ref) + } + } + } + } + return Array.from(resultSet) + } + + isMixin (_class: Ref>): boolean { + const data = this.classifiers.get(_class) + return data !== undefined && this._isMixin(data) + } + + getAncestors (_class: Ref): Ref[] { + const result = this.ancestors.get(_class) + if (result === undefined) { + throw new Error('ancestors not found: ' + _class) + } + return result + } + + getClass(_class: Ref>): Class { + const data = this.classifiers.get(_class) + if (data === undefined || this.isInterface(data)) { + throw new Error('class not found: ' + _class) + } + return data + } + + findClass(_class: Ref>): Class | undefined { + const data = this.classifiers.get(_class) + if (data === undefined || this.isInterface(data)) { + return undefined + } + return data + } + + hasClass(_class: Ref>): boolean { + const data = this.classifiers.get(_class) + + return !(data === undefined || this.isInterface(data)) + } + + getClassOrInterface (_class: Ref>): Class { + const data = this.classifiers.get(_class) + if (data === undefined) { + throw new Error('class not found: ' + _class) + } + return data + } + + getInterface (_interface: Ref>): Interface { + const data = this.classifiers.get(_interface) + if (data === undefined || !this.isInterface(data)) { + throw new Error('interface not found: ' + _interface) + } + return data + } + + getDomain (_class: Ref>): Domain { + const domain = this.findDomain(_class) + if (domain === undefined) { + throw new Error(`domain not found: ${_class} `) + } + return domain + } + + public findDomain (_class: Ref>): Domain | undefined { + const klazz = this.findClass(_class) + if (klazz === undefined) return + if (klazz.domain !== undefined) { + return klazz.domain + } + + let _klazz: Class | undefined = klazz + while (_klazz.extends !== undefined) { + _klazz = this.findClass(_klazz.extends) + if (_klazz === undefined) return + if (_klazz.domain !== undefined) { + // Cache for next requests + klazz.domain = _klazz.domain + return _klazz.domain + } + } + } + + tx (tx: Tx): void { + switch (tx._class) { + case core.class.TxCreateDoc: + this.txCreateDoc(tx as TxCreateDoc) + return + case core.class.TxUpdateDoc: + this.txUpdateDoc(tx as TxUpdateDoc) + return + case core.class.TxRemoveDoc: + this.txRemoveDoc(tx as TxRemoveDoc) + return + case core.class.TxMixin: + this.txMixin(tx as TxMixin) + } + } + + private isClassifierTx (tx: TxCUD): boolean { + const base = [core.class.Class, core.class.Mixin, core.class.Interface] + return base.includes(tx.objectClass) || this.isDerived(tx.objectClass, core.class.Class) + } + + private txCreateDoc (tx: TxCreateDoc): void { + if (this.isClassifierTx(tx)) { + const _id = tx.objectId as Ref + this.classifiers.set(_id, TxProcessor.createDoc2Doc(tx as TxCreateDoc)) + this.updateAncestors(_id) + this.updateDescendant(_id) + } else if (tx.objectClass === core.class.Attribute) { + const createTx = tx as TxCreateDoc + this.addAttribute(TxProcessor.createDoc2Doc(createTx)) + } + } + + private txUpdateDoc (tx: TxUpdateDoc): void { + if (tx.objectClass === core.class.Attribute) { + const updateTx = tx as TxUpdateDoc + const doc = this.attributesById.get(updateTx.objectId) + if (doc === undefined) return + this.addAttribute(TxProcessor.updateDoc2Doc(doc, updateTx)) + + this.classifierProperties.delete(doc.attributeOf) + } else if (this.isClassifierTx(tx)) { + const updateTx = tx as TxUpdateDoc>> + const doc = this.classifiers.get(updateTx.objectId) + if (doc === undefined) return + TxProcessor.updateDoc2Doc(doc, updateTx) + this.classifierProperties.delete(doc._id) + } + } + + private txRemoveDoc (tx: TxRemoveDoc): void { + if (tx.objectClass === core.class.Attribute) { + const removeTx = tx as TxRemoveDoc + const doc = this.attributesById.get(removeTx.objectId) + if (doc === undefined) return + const map = this.attributes.get(doc.attributeOf) + map?.delete(doc.name) + this.attributesById.delete(removeTx.objectId) + } else if (this.isClassifierTx(tx)) { + const removeTx = tx as TxRemoveDoc>> + this.updateDescendant(removeTx.objectId, false) + this.updateAncestors(removeTx.objectId, false) + this.classifiers.delete(removeTx.objectId) + } + } + + private txMixin (tx: TxMixin): void { + if (this.isClassifierTx(tx)) { + const obj = this.getClass(tx.objectId as Ref>) as any + TxProcessor.updateMixin4Doc(obj, tx) + } + } + + /** + * Check if passed _class is derived from `from` class. + * It will iterate over parents. + */ + isDerived(_class: Ref>, from: Ref>): boolean { + return this.ancestors.get(_class)?.includes(from) ?? false + } + + /** + * Return first non interface/mixin parent + */ + getBaseClass(_class: Ref>): Ref> { + let cl: Ref> | undefined = _class + while (cl !== undefined) { + const clz: Class = this.getClass(cl) + if (this.isClass(clz)) return cl + cl = clz.extends + } + return core.class.Doc + } + + /** + * Check if passed _class implements passed interfaces `from`. + * It will check for class parents and their interfaces. + */ + isImplements(_class: Ref>, from: Ref>): boolean { + let cl: Ref> | undefined = _class + while (cl !== undefined) { + const klazz: Class = this.getClass(cl) + if (this.isExtends(klazz.implements ?? [], from)) { + return true + } + cl = klazz.extends + } + return false + } + + /** + * Check if interface extends passed interface. + */ + private isExtends(extendsOrImplements: Ref>[], from: Ref>): boolean { + const result: Ref>[] = [] + const toVisit = [...extendsOrImplements] + while (toVisit.length > 0) { + const ref = toVisit.shift() as Ref> + if (ref === from) { + return true + } + addIf(result, ref) + toVisit.push(...this.ancestorsOf(ref)) + } + return false + } + + getDescendants(_class: Ref>): Ref>[] { + const data = this.descendants.get(_class) + if (data === undefined) { + throw new Error('descendants not found: ' + _class) + } + return data + } + + private updateDescendant (_class: Ref, add = true): void { + let hierarchy: Ref[] = [] + + try { + hierarchy = this.getAncestors(_class) + } catch (err) { + if (add) { + throw err + } + } + + for (const cls of hierarchy) { + const list = this.descendants.get(cls) + if (list === undefined) { + if (add) { + this.descendants.set(cls, [_class]) + } + } else { + if (add) { + list.push(_class) + } else { + const pos = list.indexOf(_class) + if (pos !== -1) { + list.splice(pos, 1) + } + } + } + } + } + + private updateAncestors (_class: Ref, add = true): void { + const cl: Ref[] = [_class] + const visited = new Set>() + const ancestorList: Ref[] = [] + + while (cl.length > 0) { + const classifier = cl.shift() as Ref + if (addNew(visited, classifier)) { + ancestorList.push(classifier) + cl.push(...this.ancestorsOf(classifier)) + } + } + + if (add) { + this.ancestors.set(_class, ancestorList) + } else { + this.ancestors.delete(_class) + } + } + + /** + * Return extends and implemnets as combined list of references + */ + private ancestorsOf (classifier: Ref): Ref[] { + const attrs = this.classifiers.get(classifier) + const result: Ref[] = [] + if (this.isClass(attrs) || this._isMixin(attrs)) { + const cls = attrs as Class + if (cls.extends !== undefined) { + result.push(cls.extends) + } + result.push(...(cls.implements ?? [])) + } + if (this.isInterface(attrs)) { + result.push(...((attrs as Interface).extends ?? [])) + } + return result + } + + private isClass (attrs?: Classifier): boolean { + return attrs?.kind === ClassifierKind.CLASS + } + + private _isMixin (attrs?: Classifier): boolean { + return attrs?.kind === ClassifierKind.MIXIN + } + + private isInterface (attrs?: Classifier): boolean { + return attrs?.kind === ClassifierKind.INTERFACE + } + + private addAttribute (attribute: AnyAttribute): void { + const _class = attribute.attributeOf + let attributes = this.attributes.get(_class) + if (attributes === undefined) { + attributes = new Map() + this.attributes.set(_class, attributes) + } + attributes.set(attribute.name, attribute) + this.attributesById.set(attribute._id, attribute) + this.classifierProperties.delete(attribute.attributeOf) + } + + getAllAttributes ( + clazz: Ref, + to?: Ref, + traverse?: (name: string, attr: AnyAttribute) => void + ): Map { + const result = new Map() + let ancestors = this.getAncestors(clazz) + if (to !== undefined) { + const toAncestors = this.getAncestors(to) + for (const uto of toAncestors) { + if (ancestors.includes(uto)) { + to = uto + break + } + } + ancestors = ancestors.filter( + (c) => c !== to && (this.isInterface(this.classifiers.get(c)) || this.isDerived(c, to as Ref>)) + ) + } + + for (let index = ancestors.length - 1; index >= 0; index--) { + const cls = ancestors[index] + const attributes = this.attributes.get(cls) + if (attributes !== undefined) { + for (const [name, attr] of attributes) { + traverse?.(name, attr) + result.set(name, attr) + } + } + } + + return result + } + + getOwnAttributes (clazz: Ref): Map { + const result = new Map() + + const attributes = this.attributes.get(clazz) + if (attributes !== undefined) { + for (const [name, attr] of attributes) { + result.set(name, attr) + } + } + + return result + } + + getParentClass (_class: Ref>): Ref> { + const baseDomain = this.getDomain(_class) + const ancestors = this.getAncestors(_class) + let result: Ref> = _class + for (const ancestor of ancestors) { + try { + const domain = this.getClass(ancestor).domain + if (domain === baseDomain) { + result = ancestor + } + } catch {} + } + return result + } + + getAttribute (classifier: Ref, name: string): AnyAttribute { + const attr = this.findAttribute(classifier, name) + if (attr === undefined) { + throw new Error('attribute not found: ' + name) + } + return attr + } + + public findAttribute (classifier: Ref, name: string): AnyAttribute | undefined { + const list = [classifier] + const visited = new Set>() + while (list.length > 0) { + const cl = list.shift() as Ref + if (addNew(visited, cl)) { + const attribute = this.attributes.get(cl)?.get(name) + if (attribute !== undefined) { + return attribute + } + // Check ancestorsOf + list.push(...this.ancestorsOf(cl)) + } + } + } + + updateLookupMixin( + _class: Ref>, + result: WithLookup, + options?: FindOptions + ): WithLookup { + const baseClass = this.getBaseClass(_class) + const vResult = baseClass !== _class ? this.as(result, _class) : result + const lookup = result.$lookup + if (lookup !== undefined) { + // We need to check if lookup type is mixin and cast to it if required. + const lu = options?.lookup as Lookup + if (lu?._id !== undefined) { + for (const [k, v] of Object.entries(lu._id)) { + const _cl = getClass(v as ToClassRefT) + if (this.isMixin(_cl)) { + const mval = (lookup as any)[k] + if (mval !== undefined) { + if (Array.isArray(mval)) { + ;(lookup as any)[k] = mval.map((it) => this.as(it, _cl)) + } else { + ;(lookup as any)[k] = this.as(mval, _cl) + } + } + } + } + } + for (const [k, v] of Object.entries(lu ?? {})) { + if (k === '_id') { + continue + } + const _cl = getClass(v as ToClassRefT) + if (this.isMixin(_cl)) { + const mval = (lookup as any)[k] + if (mval != null) { + ;(lookup as any)[k] = this.as(mval, _cl) + } + } + } + } + return vResult + } + + clone (obj: any): any { + return deepClone( + obj, + (doc, m) => this.as(doc, m), + (value) => Hierarchy.mixinClass(value) + ) + } + + domains (): Domain[] { + const classes = Array.from(this.classifiers.values()).filter( + (it) => this.isClass(it) || this._isMixin(it) + ) as Class[] + return classes + .map((it) => it.domain) + .filter((it) => it !== undefined) + .filter((it, idx, array) => array.findIndex((pt) => pt === it) === idx) + } + + getClassifierProp (cl: Ref>, prop: string): any | undefined { + return this.classifierProperties.get(cl)?.get(prop) + } + + setClassifierProp (cl: Ref>, prop: string, value: any): void { + let cur = this.classifierProperties.get(cl) + if (cur === undefined) { + cur = new Map() + this.classifierProperties.set(cl, cur) + } + cur.set(prop, value) + } +} + +function addNew (val: Set, value: T): boolean { + if (val.has(value)) { + return false + } + val.add(value) + return true +} + +function addIf (array: T[], value: T): void { + if (!array.includes(value)) { + array.push(value) + } +} + +function getClass (vvv: ToClassRefT): Ref> { + if (Array.isArray(vvv)) { + return vvv[0] + } + return vvv +} diff --git a/foundations/core/packages/core/src/index.ts b/foundations/core/packages/core/src/index.ts new file mode 100644 index 0000000000..5cc09fb86b --- /dev/null +++ b/foundations/core/packages/core/src/index.ts @@ -0,0 +1,47 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import core from './component' + +export * from './classes' +export * from './client' +export * from './collaboration' +export { + coreId, + systemAccountUuid, + readOnlyGuestAccountUuid, + systemAccountEmail, + systemAccount, + configUserAccountUuid +} from './component' +export * from './hierarchy' +export * from '@hcengineering/measurements' +export * from './memdb' +export * from './objvalue' +export * from './operations' +export * from './operator' +export * from './query' +export * from './server' +export * from './storage' +export * from './tx' +export * from './utils' +export * from './backup' +export * from './status' +export * from './clone' +export * from './common' +export * from './time' +export * from './benchmark' +export * from './collaborators' + +export default core diff --git a/foundations/core/packages/core/src/memdb.ts b/foundations/core/packages/core/src/memdb.ts new file mode 100644 index 0000000000..9feddf0201 --- /dev/null +++ b/foundations/core/packages/core/src/memdb.ts @@ -0,0 +1,399 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { PlatformError, Severity, Status } from '@hcengineering/platform' +import { type Lookup, type MeasureContext, type ReverseLookups, getObjectValue } from '.' +import type { Class, Doc, Ref } from './classes' + +import core from './component' +import { type Hierarchy } from './hierarchy' +import { checkMixinKey, matchQuery, resultSort } from './query' +import type { + AssociationQuery, + DocumentQuery, + FindOptions, + FindResult, + LookupData, + Storage, + TxResult, + WithLookup +} from './storage' +import type { Tx, TxCreateDoc, TxMixin, TxRemoveDoc, TxUpdateDoc } from './tx' +import { TxProcessor } from './tx' +import { toFindResult } from './utils' + +/** + * @public + */ +export abstract class MemDb extends TxProcessor implements Storage { + private readonly objectsByClass = new Map>, Map, Doc>>() + private readonly objectById = new Map, Doc>() + + constructor (protected readonly hierarchy: Hierarchy) { + super() + } + + private getObjectsByClass (_class: Ref>): Map, Doc> { + const result = this.objectsByClass.get(_class) + if (result === undefined) { + const result = new Map, Doc>() + this.objectsByClass.set(_class, result) + return result + } + return result + } + + private cleanObjectByClass (_class: Ref>, _id: Ref): void { + const result = this.objectsByClass.get(_class) + if (result !== undefined) { + result.delete(_id) + } + } + + private getByIdQuery(query: DocumentQuery, _class: Ref>): T[] { + const result: T[] = [] + if (typeof query._id === 'string') { + const obj = this.objectById.get(query._id) as T + if (obj !== undefined && this.hierarchy.isDerived(obj._class, _class)) result.push(obj) + } else if (query._id?.$in !== undefined) { + const ids = new Set(query._id.$in) + for (const id of ids) { + const obj = this.objectById.get(id) as T + if (obj !== undefined && this.hierarchy.isDerived(obj._class, _class)) result.push(obj) + } + } + return result + } + + getObject(_id: Ref): T { + const doc = this.objectById.get(_id) + if (doc === undefined) { + throw new PlatformError(new Status(Severity.ERROR, core.status.ObjectNotFound, { _id })) + } + return doc as T + } + + findObject(_id: Ref): T | undefined { + const doc = this.objectById.get(_id) + return doc as T + } + + private async getLookupValue( + _class: Ref>, + doc: T, + lookup: Lookup, + result: LookupData + ): Promise { + for (const key in lookup) { + if (key === '_id') { + await this.getReverseLookupValue(doc, lookup, result) + continue + } + const value = (lookup as any)[key] + const tkey = checkMixinKey(key, _class, this.hierarchy) + if (Array.isArray(value)) { + const [_class, nested] = value + const objects = await this.findAll(_class, { _id: getObjectValue(tkey, doc) }) + ;(result as any)[key] = objects[0] + const nestedResult = {} + const parent = (result as any)[key] + await this.getLookupValue(_class, parent, nested, nestedResult) + Object.assign(parent, { + $lookup: nestedResult + }) + } else { + const objects = await this.findAll(value, { _id: getObjectValue(tkey, doc) }) + ;(result as any)[key] = objects[0] + } + } + } + + private async getReverseLookupValue( + doc: T, + lookup: ReverseLookups, + result: LookupData + ): Promise { + for (const key in lookup._id) { + const value = lookup._id[key] + if (Array.isArray(value)) { + const objects = await this.findAll(value[0], { [value[1]]: doc._id }) + ;(result as any)[key] = objects + } else { + const objects = await this.findAll(value, { attachedTo: doc._id }) + ;(result as any)[key] = objects + } + } + } + + private async lookup(_class: Ref>, docs: T[], lookup: Lookup): Promise[]> { + const withLookup: WithLookup[] = [] + for (const doc of docs) { + const result: LookupData = {} + await this.getLookupValue(_class, doc, lookup, result) + withLookup.push(Object.assign({}, doc, { $lookup: result })) + } + return withLookup + } + + private async fillAssociations(docs: T[], associations: AssociationQuery[]): Promise[]> { + const withLookup: WithLookup[] = [] + for (const doc of docs) { + const result = await this.getAssociationValue(doc, associations) + withLookup.push(Object.assign({}, doc, { $associations: result })) + } + return withLookup + } + + private async getAssociationValue( + doc: T, + associations: AssociationQuery[] + ): Promise> { + const result: Record = {} + for (const association of associations) { + const _id = association[0] + const assoc = this.findObject(_id) + if (assoc === undefined) continue + const isReverse = association[1] === -1 + const key = !isReverse ? 'docA' : 'docB' + const key2 = !isReverse ? 'docB' : 'docA' + const _class = !isReverse ? assoc.classB : assoc.classA + const relations = await this.findAll(core.class.Relation, { association: _id, [key]: doc._id }) + const objects = await this.findAll(_class, { _id: { $in: relations.map((r) => r[key2]) } }) + result[_id] = objects + } + return result + } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + let result: WithLookup[] + const baseClass = this.hierarchy.getBaseClass(_class) + if ( + Object.prototype.hasOwnProperty.call(query, '_id') && + (typeof query._id === 'string' || query._id?.$in !== undefined || query._id === undefined || query._id === null) + ) { + result = this.getByIdQuery(query, baseClass) + } else { + result = Array.from(this.getObjectsByClass(baseClass).values()) + } + + result = matchQuery(result, query, _class, this.hierarchy, true) + + if (baseClass !== _class) { + // We need to filter instances without mixin was set + result = result.filter((r) => (r as any)[_class] !== undefined) + } + + if (options?.lookup !== undefined) { + result = await this.lookup(_class, result as T[], options.lookup) + result = matchQuery(result, query, _class, this.hierarchy) + } + + if (options?.associations !== undefined) { + result = await this.fillAssociations(result, options.associations) + } + + if (options?.sort !== undefined) resultSort(result, options?.sort, _class, this.hierarchy, this) + const total = result.length + result = result.slice(0, options?.limit) + const tresult = this.hierarchy.clone(result) as WithLookup[] + const res = tresult.map((it) => this.hierarchy.updateLookupMixin(_class, it, options)) + return toFindResult(res, total) + } + + async findOne( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise | undefined> { + return (await this.findAll(_class, query, { ...options, limit: 1 }))[0] + } + + /** + * Only in model find without lookups and sorting. + * Do not clone results, so be aware modifications are not allowed. + */ + findAllSync(_class: Ref>, query: DocumentQuery, options?: FindOptions): FindResult { + let result: WithLookup[] + const baseClass = this.hierarchy.getBaseClass(_class) + if ( + Object.prototype.hasOwnProperty.call(query, '_id') && + (typeof query._id === 'string' || query._id?.$in !== undefined || query._id === undefined || query._id === null) + ) { + result = this.getByIdQuery(query, baseClass) + } else { + result = Array.from(this.getObjectsByClass(baseClass).values()) + } + + result = matchQuery(result, query, _class, this.hierarchy, true) + + if (baseClass !== _class) { + // We need to filter instances without mixin was set + result = result.filter((r) => (r as any)[_class] !== undefined) + } + const total = result.length + result = result.slice(0, options?.limit) + + return toFindResult( + result.map((it) => { + return baseClass !== _class ? this.hierarchy.as(it, _class) : it + }) as WithLookup[], + total + ) + } + + addDoc (doc: Doc): void { + this.hierarchy.getAncestors(doc._class).forEach((_class) => { + const arr = this.getObjectsByClass(_class) + arr.set(doc._id, doc) + }) + + this.objectById.set(doc._id, doc) + } + + delDoc (_id: Ref): void { + const doc = this.objectById.get(_id) + if (doc === undefined) { + throw new PlatformError(new Status(Severity.ERROR, core.status.ObjectNotFound, { _id })) + } + this.objectById.delete(_id) + this.hierarchy.getAncestors(doc._class).forEach((_class) => { + this.cleanObjectByClass(_class, _id) + }) + } + + updateDoc (_id: Ref, doc: Doc, update: TxUpdateDoc | TxMixin): void { + // TODO: track updates on Contact to adjust memdb accounts? + } +} + +/** + * Hold transactions + * + * @public + */ +export class TxDb extends MemDb { + protected txCreateDoc (tx: TxCreateDoc): Promise { + throw new Error('Method not implemented.') + } + + protected txUpdateDoc (tx: TxUpdateDoc): Promise { + throw new Error('Method not implemented.') + } + + protected txRemoveDoc (tx: TxRemoveDoc): Promise { + throw new Error('Method not implemented.') + } + + protected txMixin (tx: TxMixin): Promise { + throw new Error('Method not implemented.') + } + + async tx (tx: Tx): Promise { + this.addDoc(tx) + return [] + } +} + +/** + * Hold model objects and classes + * + * @public + */ +export class ModelDb extends MemDb { + protected override async txCreateDoc (tx: TxCreateDoc): Promise { + this.addDoc(TxProcessor.createDoc2Doc(tx)) + return {} + } + + addTxes (ctx: MeasureContext, txes: Tx[], clone: boolean): void { + for (const tx of txes) { + switch (tx._class) { + case core.class.TxCreateDoc: + this.addDoc(TxProcessor.createDoc2Doc(tx as TxCreateDoc, clone)) + break + case core.class.TxUpdateDoc: { + const cud = tx as TxUpdateDoc + const doc = this.findObject(cud.objectId) + if (doc !== undefined) { + this.updateDoc(cud.objectId, doc, cud) + TxProcessor.updateDoc2Doc(doc, cud) + } else { + ctx.warn('no document found, failed to apply model transaction, skipping', { + _id: tx._id, + _class: tx._class, + objectId: cud.objectId + }) + } + break + } + case core.class.TxRemoveDoc: + try { + this.delDoc((tx as TxRemoveDoc).objectId) + } catch (err: any) { + ctx.warn('no document found, failed to apply model transaction, skipping', { + _id: tx._id, + _class: tx._class, + objectId: (tx as TxRemoveDoc).objectId + }) + } + break + case core.class.TxMixin: { + const mix = tx as TxMixin + const doc = this.findObject(mix.objectId) + if (doc !== undefined) { + this.updateDoc(mix.objectId, doc, mix) + TxProcessor.updateMixin4Doc(doc, mix) + } else { + ctx.warn('no document found, failed to apply model transaction, skipping', { + _id: tx._id, + _class: tx._class, + objectId: mix.objectId + }) + } + break + } + } + } + } + + protected async txUpdateDoc (tx: TxUpdateDoc): Promise { + try { + const doc = this.getObject(tx.objectId) as any + this.updateDoc(tx.objectId, doc, tx) + TxProcessor.updateDoc2Doc(doc, tx) + return tx.retrieve === true ? { object: doc } : {} + } catch (err: any) {} + return {} + } + + protected async txRemoveDoc (tx: TxRemoveDoc): Promise { + try { + this.delDoc(tx.objectId) + } catch (err: any) {} + return {} + } + + // TODO: process ancessor mixins + protected async txMixin (tx: TxMixin): Promise { + const doc = this.getObject(tx.objectId) as any + this.updateDoc(tx.objectId, doc, tx) + TxProcessor.updateMixin4Doc(doc, tx) + return {} + } +} diff --git a/foundations/core/packages/core/src/objvalue.ts b/foundations/core/packages/core/src/objvalue.ts new file mode 100644 index 0000000000..8a7df8ed7a --- /dev/null +++ b/foundations/core/packages/core/src/objvalue.ts @@ -0,0 +1,81 @@ +import { PlatformError, Severity, Status } from '@hcengineering/platform' +import { type Doc } from './classes' +import { clone } from './clone' +import core from './component' + +/** + * @public + */ +export function getObjectValue (key: string, doc: Doc): any { + // Check dot notation + if (key.length === 0) { + return doc + } + key = key.split('\\$').join('$') + const dots = key.split('.') + // Replace escapting, since memdb is not escape keys + + // We have dots, so iterate in depth + let pos = 0 + let value = doc as any + for (const d of dots) { + if (Array.isArray(value) && isNestedArrayQuery(value, d)) { + // Array and d is not an indexed field. + // So return array of nested values. + return getNestedArrayValue(value, dots.slice(pos).join('.')) + } + value = value?.[d] + pos++ + } + return value +} + +/** + * @public + */ +export function setObjectValue (key: string, doc: Doc, newValue: any): void { + // Check dot notation + if (key.length === 0) { + return + } + key = key.split('\\$').join('$') + let dots = key.split('.') + // Replace escapting, since memdb is not escape keys + + const last = dots[dots.length - 1] + dots = dots.slice(0, -1) + + // We have dots, so iterate in depth + let value = doc as any + for (const d of dots) { + if (Array.isArray(value) && isNestedArrayQuery(value, d)) { + // Arrays are not supported + throw new PlatformError(new Status(Severity.ERROR, core.status.ObjectNotFound, { _id: 'dots' })) + } + const lvalue = value?.[d] + if (lvalue === undefined) { + value[d] = {} + value = value?.[d] + } else { + value = lvalue + } + } + value[last] = clone(newValue) + return value +} + +function isNestedArrayQuery (value: any, d: string): boolean { + return Number.isNaN(Number.parseInt(d)) && value?.[d as any] === undefined +} + +function getNestedArrayValue (value: any[], name: string): any[] { + const result = [] + for (const v of value) { + result.push(...arrayOrValue(getObjectValue(name, v))) + } + return result +} + +function arrayOrValue (vv: any): any[] { + return Array.isArray(vv) ? vv : [vv] +} diff --git a/foundations/core/packages/core/src/operations.ts b/foundations/core/packages/core/src/operations.ts new file mode 100644 index 0000000000..8a182e5514 --- /dev/null +++ b/foundations/core/packages/core/src/operations.ts @@ -0,0 +1,653 @@ +import { Analytics } from '@hcengineering/analytics' +import { deepEqual } from 'fast-equals' +import { + type DocumentUpdate, + DOMAIN_MODEL, + Hierarchy, + type MixinData, + type MixinUpdate, + type ModelDb, + platformNow, + toFindResult +} from '.' +import type { + AnyAttribute, + AttachedData, + AttachedDoc, + Class, + Data, + Doc, + Mixin, + OperationDomain, + PersonId, + Ref, + Space, + Timestamp +} from './classes' +import { type Client } from './client' +import core from './component' +import type { + DocumentQuery, + DomainParams, + DomainResult, + FindOptions, + FindResult, + SearchOptions, + SearchQuery, + SearchResult, + TxResult, + WithLookup +} from './storage' +import { type DocumentClassQuery, type Tx, type TxApplyResult, type TxCUD, TxFactory, TxProcessor } from './tx' + +/** + * @public + * + * High Level operations with client, will create low level transactions. + * + * `notify` is not supported by TxOperations. + */ +export class TxOperations implements Omit { + readonly txFactory: TxFactory + + constructor ( + readonly client: Client, + readonly user: PersonId, + readonly isDerived: boolean = false + ) { + this.txFactory = new TxFactory(user, isDerived) + } + + getHierarchy (): Hierarchy { + return this.client.getHierarchy() + } + + getModel (): ModelDb { + return this.client.getModel() + } + + async close (): Promise { + await this.client.close() + } + + findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions | undefined + ): Promise> { + return this.client.findAll(_class, query, options) + } + + findOne( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions | undefined + ): Promise | undefined> { + return this.client.findOne(_class, query, options) + } + + domainRequest(domain: OperationDomain, params: DomainParams): Promise> { + return this.client.domainRequest(domain, params) + } + + searchFulltext (query: SearchQuery, options: SearchOptions): Promise { + return this.client.searchFulltext(query, options) + } + + tx (tx: Tx): Promise { + return this.client.tx(tx) + } + + async createDoc( + _class: Ref>, + space: Ref, + attributes: Data, + id?: Ref, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): Promise> { + const hierarchy = this.client.getHierarchy() + if (hierarchy.isDerived(_class, core.class.AttachedDoc)) { + throw new Error('createDoc cannot be used for objects inherited from AttachedDoc') + } + if (hierarchy.findDomain(_class) === DOMAIN_MODEL && space !== core.space.Model) { + throw new Error('createDoc cannot be called for DOMAIN_MODEL classes with non-model space') + } + const tx = this.txFactory.createTxCreateDoc(_class, space, attributes, id, modifiedOn, modifiedBy) + await this.tx(tx) + return tx.objectId + } + + async addCollection( + _class: Ref>, + space: Ref, + attachedTo: Ref, + attachedToClass: Ref>, + collection: Extract | string, + attributes: AttachedData

, + id?: Ref

, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): Promise> { + const tx = this.txFactory.createTxCollectionCUD( + attachedToClass, + attachedTo, + space, + collection, + this.txFactory.createTxCreateDoc

(_class, space, attributes as unknown as Data

, id, modifiedOn, modifiedBy), + modifiedOn, + modifiedBy + ) + await this.tx(tx) + return tx.objectId as unknown as Ref

+ } + + async updateCollection( + _class: Ref>, + space: Ref, + objectId: Ref

, + attachedTo: Ref, + attachedToClass: Ref>, + collection: Extract | string, + operations: DocumentUpdate

, + retrieve?: boolean, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): Promise> { + const tx = this.txFactory.createTxCollectionCUD( + attachedToClass, + attachedTo, + space, + collection, + this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve, modifiedOn, modifiedBy), + modifiedOn, + modifiedBy + ) + await this.tx(tx) + return attachedTo + } + + async removeCollection( + _class: Ref>, + space: Ref, + objectId: Ref

, + attachedTo: Ref, + attachedToClass: Ref>, + collection: Extract | string, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): Promise> { + const tx = this.txFactory.createTxCollectionCUD( + attachedToClass, + attachedTo, + space, + collection, + this.txFactory.createTxRemoveDoc(_class, space, objectId, modifiedOn, modifiedBy), + modifiedOn, + modifiedBy + ) + await this.tx(tx) + return attachedTo + } + + updateDoc( + _class: Ref>, + space: Ref, + objectId: Ref, + operations: DocumentUpdate, + retrieve?: boolean, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): Promise { + const tx = this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve, modifiedOn, modifiedBy) + return this.tx(tx) + } + + removeDoc( + _class: Ref>, + space: Ref, + objectId: Ref, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): Promise { + const tx = this.txFactory.createTxRemoveDoc(_class, space, objectId, modifiedOn, modifiedBy) + return this.tx(tx) + } + + createMixin( + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + mixin: Ref>, + attributes: MixinData, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): Promise { + const tx = this.txFactory.createTxMixin( + objectId, + objectClass, + objectSpace, + mixin, + attributes, + modifiedOn, + modifiedBy + ) + return this.tx(tx) + } + + updateMixin( + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + mixin: Ref>, + attributes: MixinUpdate, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): Promise { + const tx = this.txFactory.createTxMixin( + objectId, + objectClass, + objectSpace, + mixin, + attributes, + modifiedOn, + modifiedBy + ) + return this.tx(tx) + } + + async update( + doc: T, + update: DocumentUpdate, + retrieve?: boolean, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): Promise { + const hierarchy = this.client.getHierarchy() + const mixClass = Hierarchy.mixinOrClass(doc) + if (hierarchy.isMixin(mixClass)) { + const baseClass = hierarchy.getBaseClass(doc._class) + + const byClass = splitMixinUpdate(hierarchy, update, mixClass, baseClass) + const ops = this.apply(doc._id) + for (const it of byClass) { + if (hierarchy.isMixin(it[0])) { + await ops.updateMixin(doc._id, baseClass, doc.space, it[0], it[1], modifiedOn, modifiedBy) + } else { + if (hierarchy.isDerived(it[0], core.class.AttachedDoc)) { + const adoc = doc as unknown as AttachedDoc + return await this.updateCollection( + it[0], + doc.space, + adoc._id, + adoc.attachedTo, + adoc.attachedToClass, + adoc.collection, + it[1], + retrieve, + modifiedOn, + modifiedBy + ) + } + await ops.updateDoc(it[0], doc.space, doc._id, it[1], retrieve, modifiedOn, modifiedBy) + } + } + return await ops.commit() + } + if (hierarchy.isDerived(doc._class, core.class.AttachedDoc)) { + const adoc = doc as unknown as AttachedDoc + return await this.updateCollection( + doc._class, + doc.space, + adoc._id, + adoc.attachedTo, + adoc.attachedToClass, + adoc.collection, + update, + retrieve, + modifiedOn, + modifiedBy + ) + } + return await this.updateDoc(doc._class, doc.space, doc._id, update, retrieve, modifiedOn, modifiedBy) + } + + remove(doc: T, modifiedOn?: Timestamp, modifiedBy?: PersonId): Promise { + if (this.client.getHierarchy().isDerived(doc._class, core.class.AttachedDoc)) { + const adoc = doc as unknown as AttachedDoc + return this.removeCollection( + doc._class, + doc.space, + adoc._id, + adoc.attachedTo, + adoc.attachedToClass, + adoc.collection, + modifiedOn, + modifiedBy + ) + } + return this.removeDoc(doc._class, doc.space, doc._id) + } + + apply (scope?: string, measure?: string, derived?: boolean): ApplyOperations { + return new ApplyOperations(this, scope, measure, derived ?? this.isDerived) + } + + async diffUpdate( + doc: T, + update: T | Data | DocumentUpdate, + date?: Timestamp, + account?: PersonId + ): Promise { + const documentUpdate = getDiffUpdate(doc, update) + if (Object.keys(documentUpdate).length > 0) { + await this.update(doc, documentUpdate, false, date ?? Date.now(), account) + TxProcessor.applyUpdate(doc, documentUpdate) + } + return doc + } + + async mixinDiffUpdate ( + doc: Doc, + raw: Doc | Data, + mixin: Ref>>, + modifiedBy: PersonId, + modifiedOn: Timestamp + ): Promise { + // We need to update fields if they are different. + + if (!this.getHierarchy().hasMixin(doc, mixin)) { + await this.createMixin(doc._id, doc._class, doc.space, mixin, raw as MixinData, modifiedOn, modifiedBy) + TxProcessor.applyUpdate(this.getHierarchy().as(doc, mixin), raw) + return doc + } + + const documentUpdate: MixinUpdate = {} + for (const [k, v] of Object.entries(raw)) { + if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) { + continue + } + const dv = (doc as any)[k] + if (!deepEqual(dv, v) && v != null) { + ;(documentUpdate as any)[k] = v + } + } + if (Object.keys(documentUpdate).length > 0) { + await this.updateMixin(doc._id, doc._class, doc.space, mixin, documentUpdate, modifiedOn, modifiedBy) + TxProcessor.applyUpdate(this.getHierarchy().as(doc, mixin), documentUpdate) + } + return doc + } +} + +export function getDiffUpdate (doc: T, update: T | Data | DocumentUpdate): DocumentUpdate { + // We need to update fields if they are different. + const documentUpdate: DocumentUpdate = {} + for (const [k, v] of Object.entries(update)) { + if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) { + continue + } + const dv = (doc as any)[k] + if (!deepEqual(dv, v) && v !== undefined) { + ;(documentUpdate as any)[k] = v + } + } + return documentUpdate +} + +export function splitMixinUpdate ( + hierarchy: Hierarchy, + update: DocumentUpdate, + mixClass: Ref>, + baseClass: Ref> +): Map>, DocumentUpdate> { + const attributes = hierarchy.getAllAttributes(mixClass) + + const updateAttrs = Object.fromEntries( + Object.entries(update).filter((it) => !it[0].startsWith('$')) + ) as DocumentUpdate + const updateOps = Object.fromEntries( + Object.entries(update).filter((it) => it[0].startsWith('$')) + ) as DocumentUpdate + + const result: Map>, DocumentUpdate> = splitObjectAttributes( + hierarchy, + updateAttrs, + baseClass, + attributes + ) + + for (const [key, value] of Object.entries(updateOps)) { + const updates = splitObjectAttributes(hierarchy, value as object, baseClass, attributes) + + for (const [opsClass, opsUpdate] of updates) { + const upd: DocumentUpdate = result.get(opsClass) ?? {} + result.set(opsClass, { ...upd, [key]: opsUpdate }) + } + } + + return result +} + +function splitObjectAttributes ( + hierarchy: Hierarchy, + obj: T, + objClass: Ref>, + attributes: Map +): Map>, object> { + const result = new Map>, any>() + for (const [key, value] of Object.entries(obj)) { + const attributeOf = attributes.get(key)?.attributeOf + const clazz = attributeOf !== undefined && hierarchy.isMixin(attributeOf) ? attributeOf : objClass + result.set(clazz, { ...(result.get(clazz) ?? {}), [key]: value }) + } + + return result +} + +export interface CommitResult { + result: boolean + time: number + serverTime: number +} + +/** + * @public + * + * Builder for ApplyOperation, with same syntax as TxOperations. + * + * Will send real command on commit and will return boolean of operation success. + */ +export class ApplyOperations extends TxOperations { + txes: TxCUD[] = [] + matches: DocumentClassQuery[] = [] + notMatches: DocumentClassQuery[] = [] + constructor ( + readonly ops: TxOperations, + readonly scope?: string, + readonly measureName?: string, + isDerived?: boolean + ) { + const txClient: Client = { + getHierarchy: () => ops.client.getHierarchy(), + getModel: () => ops.client.getModel(), + close: () => ops.client.close(), + findOne: (_class, query, options?) => ops.client.findOne(_class, query, options), + findAll: (_class, query, options?) => ops.client.findAll(_class, query, options), + searchFulltext: (query, options) => ops.client.searchFulltext(query, options), + domainRequest: (domain, params) => ops.client.domainRequest(domain, params), + tx: async (tx): Promise => { + if (TxProcessor.isExtendsCUD(tx._class)) { + this.txes.push(tx as TxCUD) + } + return {} + } + } + super(txClient, ops.user, isDerived ?? false) + } + + match(_class: Ref>, query: DocumentQuery): ApplyOperations { + this.matches.push({ _class, query }) + return this + } + + notMatch(_class: Ref>, query: DocumentQuery): ApplyOperations { + this.notMatches.push({ _class, query }) + return this + } + + async commit (notify: boolean = true, extraNotify: Ref>[] = []): Promise { + if ( + this.txes.length === 1 && + this.matches.length === 0 && + this.notMatches.length === 0 && + this.measureName == null + ) { + const st = platformNow() + // Individual update, no need for apply + await this.ops.tx(this.txes[0]) + const time = platformNow() - st + this.txes = [] + return { + result: true, + time, + serverTime: time + } + } + if (this.txes.length > 0) { + const st = platformNow() + const aop = this.ops.txFactory.createTxApplyIf( + core.space.Tx, + this.scope, + this.matches, + this.notMatches, + this.txes, + this.measureName, + notify, + extraNotify + ) + const result = (await this.ops.tx(aop)) as TxApplyResult + const dnow = platformNow() + if (typeof window === 'object' && window !== null && this.measureName != null) { + console.log(`measure ${this.measureName}`, dnow - st, 'server time', result.serverTime) + } + this.txes = [] + return { + result: result.success, + time: dnow - st, + serverTime: result.serverTime + } + } + return { result: true, time: 0, serverTime: 0 } + } + + // Apply for this will reuse, same apply context. + apply (scope?: string, measure?: string): ApplyOperations { + return this + } +} + +/** + * @public + * + * Builder for TxOperations. + */ +export class TxBuilder extends TxOperations { + txes: TxCUD[] = [] + matches: DocumentClassQuery[] = [] + constructor ( + readonly hierarchy: Hierarchy, + readonly modelDb: ModelDb, + user: PersonId + ) { + const txClient: Client = { + getHierarchy: () => this.hierarchy, + getModel: () => this.modelDb, + close: async () => {}, + findOne: async (_class, query, options?) => undefined, + findAll: async (_class, query, options?) => toFindResult([]), + searchFulltext: async (query, options) => ({ docs: [] }), + domainRequest: async (domain, params) => ({ domain, value: null as any }), + tx: async (tx): Promise => { + if (TxProcessor.isExtendsCUD(tx._class)) { + this.txes.push(tx as TxCUD) + } + return {} + } + } + super(txClient, user) + } +} + +/** + * @public + */ +export async function updateAttribute ( + client: TxOperations, + object: Doc, + _class: Ref>, + attribute: { key: string, attr: AnyAttribute }, + value: any, + saveModified: boolean = false, + analyticsProps: Record = {} +): Promise { + const doc = object + const attributeKey = attribute.key + if ((doc as any)[attributeKey] === value) return + const modifiedOn = saveModified ? doc.modifiedOn : Date.now() + const modifiedBy = attribute.key === 'modifiedBy' ? value : saveModified ? doc.modifiedBy : undefined + const attr = attribute.attr + + const baseAnalyticsProps = { + objectClass: _class, + objectId: object._id, + attribute: attributeKey, + ...analyticsProps + } + if (client.getHierarchy().isMixin(attr.attributeOf)) { + await client.updateMixin( + doc._id, + _class, + doc.space, + attr.attributeOf, + { [attributeKey]: value }, + modifiedOn, + modifiedBy + ) + Analytics.handleEvent('ChangeAttribute', { ...baseAnalyticsProps, value }) + } else { + if (client.getHierarchy().isDerived(attribute.attr.type._class, core.class.ArrOf)) { + const oldValue: any[] = (object as any)[attributeKey] ?? [] + const val: any[] = Array.isArray(value) ? value : [value] + const toPull = oldValue.filter((it: any) => !val.includes(it)) + + const toPush = val.filter((it) => !oldValue.includes(it)) + if (toPull.length > 0) { + await client.update(object, { $pull: { [attributeKey]: { $in: toPull } } }, false, modifiedOn, modifiedBy) + Analytics.handleEvent('RemoveCollectionItems', { + ...baseAnalyticsProps, + removed: toPull + }) + } + if (toPush.length > 0) { + await client.update( + object, + { $push: { [attributeKey]: { $each: toPush, $position: 0 } } }, + false, + modifiedOn, + modifiedBy + ) + Analytics.handleEvent('AddCollectionItems', { + ...baseAnalyticsProps, + added: toPush + }) + } + } else { + await client.update(object, { [attributeKey]: value }, false, modifiedOn, modifiedBy) + Analytics.handleEvent('SetCollectionItems', { + ...baseAnalyticsProps, + value + }) + } + } +} diff --git a/foundations/core/packages/core/src/operator.ts b/foundations/core/packages/core/src/operator.ts new file mode 100644 index 0000000000..232b555c9e --- /dev/null +++ b/foundations/core/packages/core/src/operator.ts @@ -0,0 +1,209 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Analytics } from '@hcengineering/analytics' +import type { Doc, PropertyType } from './classes' +import type { Position, PullArray, QueryUpdate } from './tx' + +/** + * @internal + */ +export type _OperatorFunc = (doc: Doc, op: any) => void + +function $push (document: Doc, keyval: Record): void { + const doc = document as any + for (const key in keyval) { + if (doc[key] === undefined) { + doc[key] = [] + } + const kvk = keyval[key] + if (typeof kvk === 'object' && kvk != null) { + const arr = doc[key] as Array + const desc = kvk as Position + if ('$each' in desc) { + if (arr != null && Array.isArray(arr)) { + arr.splice(desc.$position ?? 0, 0, ...desc.$each) + } + } else { + arr.push(kvk) + } + } else { + if (doc[key] === null || doc[key] === undefined) { + doc[key] = [kvk] + } else { + if (Array.isArray(doc[key])) { + doc[key].push(kvk) + } else { + Analytics.handleError(new Error(`invalid array value: ${JSON.stringify(doc[key])} `)) + doc[key] = [kvk] + } + } + } + } +} + +function $pull (document: Doc, keyval: Record): void { + const doc = document as any + for (const key in keyval) { + if (doc[key] === undefined) { + doc[key] = [] + } + // Ensure doc[key] is an array before attempting to filter + if (!Array.isArray(doc[key])) { + Analytics.handleError(new Error(`$pull operation on non-array field: ${key}, value: ${JSON.stringify(doc[key])}`)) + doc[key] = [] + continue + } + const arr = doc[key] + const kvk = keyval[key] + if (typeof kvk === 'object' && kvk !== null) { + const { $in } = kvk as PullArray + + doc[key] = (arr ?? []).filter((val) => { + if ($in !== undefined) { + return !$in.includes(val) + } else { + // We need to match all fields + for (const [kk, kv] of Object.entries(kvk)) { + if (val[kk] !== kv) { + return true + } + } + return false + } + }) + } else { + doc[key] = (arr ?? []).filter((val) => val !== kvk) + } + } +} + +function matchArrayElement (docs: any[], query: Partial): any[] { + let result = [...docs] + for (const key in query) { + const value = (query as any)[key] + + const tresult: any[] = [] + for (const object of result) { + const val = object[key] + if (val === value) { + tresult.push(object) + } + } + result = tresult + if (tresult.length === 0) { + break + } + } + return result +} + +function $update (document: Doc, keyval: Record): void { + const doc = document as any + for (const key in keyval) { + if (doc[key] === undefined) { + doc[key] = [] + } + // Ensure doc[key] is an array before attempting to update + if (!Array.isArray(doc[key])) { + Analytics.handleError(new Error(`$update operation on non-array field: ${key}, value: ${JSON.stringify(doc[key])}`)) + doc[key] = [] + continue + } + const val = keyval[key] + if (typeof val === 'object') { + const arr = doc[key] + const desc = val as QueryUpdate + for (const m of matchArrayElement(arr, desc.$query)) { + for (const [k, v] of Object.entries(desc.$update)) { + m[k] = v + } + } + } + } +} + +function $inc (document: Doc, keyval: Record): void { + const doc = document as unknown as Record + for (const key in keyval) { + const cur = doc[key] ?? 0 + // Ensure current value is a number + if (typeof cur !== 'number' || isNaN(cur)) { + Analytics.handleError(new Error(`$inc operation on non-numeric field: ${key}, value: ${JSON.stringify(doc[key])}`)) + doc[key] = keyval[key] + continue + } + const increment = keyval[key] + // Ensure increment value is a valid number + if (typeof increment !== 'number' || isNaN(increment)) { + Analytics.handleError(new Error(`$inc operation with invalid increment: ${key}, increment: ${JSON.stringify(increment)}`)) + continue + } + doc[key] = cur + increment + } +} + +function $unset (document: Doc, keyval: Record): void { + const doc = document as any + for (const key in keyval) { + if (doc[key] !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete doc[key] + } + } +} + +function $rename (document: Doc, keyval: Record): void { + const doc = document as any + for (const key in keyval) { + if (doc[key] !== undefined) { + doc[keyval[key]] = doc[key] + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete doc[key] + } + } +} + +const operators: Record = { + $push, + $pull, + $update, + $inc, + $unset, + $rename +} + +/** + * @public + */ +export function isOperator (o: Record): boolean { + if (o === null || typeof o !== 'object') { + return false + } + const keys = Object.keys(o) + return keys.length > 0 && keys.every((key) => key.startsWith('$')) +} + +/** + * @internal + * @param name - + * @returns + */ +export function _getOperator (name: string): _OperatorFunc { + const operator = operators[name] + if (operator === undefined) throw new Error('unknown operator: ' + name) + return operator +} diff --git a/foundations/core/packages/core/src/predicate.ts b/foundations/core/packages/core/src/predicate.ts new file mode 100644 index 0000000000..4fbdc47dd1 --- /dev/null +++ b/foundations/core/packages/core/src/predicate.ts @@ -0,0 +1,157 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Doc } from './classes' +import { getObjectValue } from './objvalue' +import { escapeLikeForRegexp } from './utils' + +import { deepEqual } from 'fast-equals' + +type Predicate = (docs: Doc[]) => Doc[] +type PredicateFactory = (pred: any, propertyKey: string) => Predicate + +type ExecPredicate = (value: any) => boolean + +function execPredicate (docs: Doc[], propertyKey: string, pred: ExecPredicate): Doc[] { + const result: Doc[] = [] + for (const doc of docs) { + const value = getObjectValue(propertyKey, doc) + if (pred(value)) { + result.push(doc) + } + } + return result +} + +const predicates: Record = { + $in: (o, propertyKey) => { + if (!Array.isArray(o)) { + throw new Error('$in predicate requires array') + } + return (docs) => + execPredicate(docs, propertyKey, (value) => { + if (Array.isArray(value)) { + return o.some((p) => value.includes(p)) + } else { + // eslint-disable-next-line eqeqeq + return o.some((p) => p == value) + } + }) + }, + $all: (o, propertyKey) => { + if (!Array.isArray(o)) { + throw new Error('$all predicate requires array') + } + return (docs) => + execPredicate(docs, propertyKey, (value: any[]) => { + for (const val of o) { + if (!value.includes(val)) return false + } + return true + }) + }, + $nin: (o, propertyKey) => { + if (!Array.isArray(o)) { + throw new Error('$nin predicate requires array') + } + return (docs) => + execPredicate(docs, propertyKey, (value) => { + if (Array.isArray(value)) { + return !o.some((p) => value.includes(p)) + } else { + // eslint-disable-next-line eqeqeq + return !o.some((p) => p == value) + } + }) + }, + + $like: (query: string, propertyKey: string): Predicate => { + const searchString = query + .split('%') + .map((it) => escapeLikeForRegexp(it)) + .join('.*') + const regex = RegExp(`^${searchString}$`, 'i') + + return (docs) => execPredicate(docs, propertyKey, (value) => regex.test(value)) + }, + + $regex: (o: { $regex: string, $options: string }, propertyKey: string): Predicate => { + const re = new RegExp(o.$regex, o.$options) + return (docs) => execPredicate(docs, propertyKey, (value) => value.match(re) !== null) + }, + $gt: (o, propertyKey) => { + return (docs) => execPredicate(docs, propertyKey, (value) => value > o) + }, + $gte: (o, propertyKey) => { + return (docs) => execPredicate(docs, propertyKey, (value) => value >= o) + }, + $lt: (o, propertyKey) => { + return (docs) => execPredicate(docs, propertyKey, (value) => value < o) + }, + $lte: (o, propertyKey) => { + return (docs) => execPredicate(docs, propertyKey, (value) => value <= o) + }, + $exists: (o, propertyKey) => { + return (docs) => execPredicate(docs, propertyKey, (value) => (value !== undefined) === o) + }, + $ne: (o, propertyKey) => { + // eslint-disable-next-line eqeqeq + return (docs) => execPredicate(docs, propertyKey, (value) => (o != null ? !deepEqual(o, value) : value != null)) + }, + $size: (o, propertyKey) => { + return (docs) => + execPredicate(docs, propertyKey, (value) => { + if (!Array.isArray(value)) { + throw new Error('$size predicate requires array') + } + if (typeof o === 'number') { + return value.length === o + } + if (typeof o === 'object' && o.$gt !== undefined) { + return value.length > o.$gt + } + if (typeof o === 'object' && o.$gte !== undefined) { + return value.length >= o.$gte + } + if (typeof o === 'object' && o.$lt !== undefined) { + return value.length < o.$lt + } + if (typeof o === 'object' && o.$lte !== undefined) { + return value.length <= o.$lte + } + return false + }) + } +} + +export function isPredicate (o: Record): boolean { + if (o === null || typeof o !== 'object') { + return false + } + const keys = Object.keys(o) + return keys.length > 0 && keys.every((key) => key.startsWith('$')) +} + +export function createPredicates (o: Record, propertyKey: string): Predicate[] { + const keys = Object.keys(o) + const result: Predicate[] = [] + for (const key of keys) { + const factory = predicates[key] + if (factory === undefined) throw new Error('unknown predicate: ' + keys[0]) + result.push(factory(o[key], propertyKey)) + } + return result +} diff --git a/foundations/core/packages/core/src/proxy.ts b/foundations/core/packages/core/src/proxy.ts new file mode 100644 index 0000000000..d5ab1abde6 --- /dev/null +++ b/foundations/core/packages/core/src/proxy.ts @@ -0,0 +1,86 @@ +import { PlatformError, unknownError } from '@hcengineering/platform' +import { type Ref } from '.' +import type { Doc, Mixin } from './classes' + +const PROXY_TARGET_KEY = '$___proxy_target' +export const PROXY_MIXIN_CLASS_KEY = '$__mixin' + +/** + * @internal + */ +export function _createMixinProxy (mixin: Mixin, ancestorProxy: ProxyHandler | null): ProxyHandler { + return { + get (target: any, property: string, receiver: any): any { + if (property === PROXY_TARGET_KEY) { + return target + } + // We need to override _class property, to return proper mixin class. + if (property === PROXY_MIXIN_CLASS_KEY) { + return mixin._id + } + const value = target[mixin._id]?.[property] + if (value === undefined) { + return ancestorProxy !== null ? ancestorProxy.get?.(target, property, receiver) : target[property] + } + return value + } + } +} + +export function freeze (value: any): any { + if (value != null && typeof value === 'object') { + if (Array.isArray(value)) { + return value.map((it) => freeze(it)) + } + if (value instanceof Map) { + throw new PlatformError(unknownError('Map is not allowed in model')) + } + if (value instanceof Set) { + throw new PlatformError(unknownError('Set is not allowed in model')) + } + return new Proxy(value, _createFreezeProxy(value)) + } + return value +} +/** + * @internal + */ +export function _createFreezeProxy (doc: Doc): ProxyHandler { + return { + get (target: any, property: string, receiver: any): any { + const value = target[property] + return freeze(value) + }, + set (target, p, newValue, receiver): any { + throw new PlatformError(unknownError('Modification is not allowed')) + }, + defineProperty (target, property, attributes): any { + throw new PlatformError(unknownError('Modification is not allowed')) + }, + + deleteProperty (target, p): any { + throw new PlatformError(unknownError('Modification is not allowed')) + }, + setPrototypeOf (target, v): any { + throw new PlatformError(unknownError('Modification is not allowed')) + } + } +} + +/** + * @internal + */ +export function _toDoc (doc: D): D { + const targetDoc = (doc as any)[PROXY_TARGET_KEY] + if (targetDoc !== undefined) { + return targetDoc as D + } + return doc +} + +/** + * @internal + */ +export function _mixinClass (doc: D): Ref> | undefined { + return (doc as any)[PROXY_MIXIN_CLASS_KEY] +} diff --git a/foundations/core/packages/core/src/query.ts b/foundations/core/packages/core/src/query.ts new file mode 100644 index 0000000000..81f20d9a97 --- /dev/null +++ b/foundations/core/packages/core/src/query.ts @@ -0,0 +1,194 @@ +import { type DocumentQuery, type MemDb } from '.' +import { type Class, type Doc, type Enum, type EnumOf, type Ref } from './classes' +import core from './component' +import { type Hierarchy } from './hierarchy' +import { getObjectValue } from './objvalue' +import { createPredicates, isPredicate } from './predicate' +import { type SortQuerySelector, type SortingOrder, type SortingQuery, type SortingRules } from './storage' + +/** + * @public + */ +export function findProperty (objects: Doc[], propertyKey: string, value: any): Doc[] { + if (isPredicate(value)) { + const preds = createPredicates(value, propertyKey) + for (const pred of preds) { + objects = pred(objects) + } + return objects + } + const result: Doc[] = [] + for (const object of objects) { + const val = getObjectValue(propertyKey, object) + if (val === value || (val == null && value == null) || isArrayValueCheck(val, value)) { + result.push(object) + } + } + return result +} + +function isArrayValueCheck (val: T, value: P): boolean { + return Array.isArray(val) && !Array.isArray(value) && val.includes(value) +} + +function getEnumValue ( + key: string, + _class: Ref>, + hierarchy: Hierarchy, + obj: any, + _enum: Enum +): number { + const tkey = checkMixinKey(key, _class, hierarchy) + const value = getObjectValue(tkey, obj) + const index = _enum.enumValues.findIndex((p) => p === value) + return index === -1 ? _enum.enumValues.length : index +} + +/** + * @public + */ +export function resultSort ( + result: T[], + sortOptions: SortingQuery, + _class: Ref>, + hierarchy: Hierarchy, + modelDb: MemDb +): void { + const enums = getEnums(_class, sortOptions, hierarchy, modelDb) + const sortFunc = (a: any, b: any): number => { + for (const key in sortOptions) { + const _enum = enums[key] + const aValue = + _enum !== undefined ? getEnumValue(key, _class, hierarchy, a, _enum) : getValue(key, a, _class, hierarchy) + const bValue = + _enum !== undefined ? getEnumValue(key, _class, hierarchy, b, _enum) : getValue(key, b, _class, hierarchy) + const result = getSortingResult(aValue, bValue, sortOptions[key]) + if (result !== 0) return result + } + return 0 + } + result.sort(sortFunc) +} + +function mapSortingValue (order: SortingOrder | SortingRules, val: any): any { + if (typeof order !== 'object') { + return val + } + for (const r of order.cases) { + if (typeof r.query === 'object') { + const q: SortQuerySelector = r.query + if (q.$in?.includes(val) ?? false) { + return r.index + } + if (q.$nin !== undefined && !q.$nin.includes(val)) { + return r.index + } + if (q.$ne !== undefined && q.$ne !== val) { + return r.index + } + } + if (r.query === val) { + return r.index + } + } +} + +function getSortingResult (aValue: any, bValue: any, order: SortingOrder | SortingRules): number { + let res = 0 + if (typeof aValue === 'undefined') { + return typeof bValue === 'undefined' ? 0 : -1 + } + if (typeof bValue === 'undefined') { + return 1 + } + + const orderOrder = typeof order === 'object' ? order.order : order + + if (Array.isArray(aValue) && Array.isArray(bValue)) { + res = + (aValue.map((it) => mapSortingValue(order, it)).sort((a, b) => (a - b) * orderOrder)[0] ?? 0) - + (bValue.map((it) => mapSortingValue(order, it)).sort((a, b) => (a - b) * orderOrder)[0] ?? 0) + } else { + const aaValue = mapSortingValue(order, aValue) + const bbValue = mapSortingValue(order, bValue) + res = typeof aaValue === 'string' ? aaValue.localeCompare(bbValue) : aaValue - bbValue + } + return res * orderOrder +} + +function getEnums ( + _class: Ref>, + sortOptions: SortingQuery, + hierarchy: Hierarchy, + modelDb: MemDb +): Record { + const res: Record = {} + for (const key in sortOptions) { + const attr = hierarchy.findAttribute(_class, key) + if (attr !== undefined) { + if (attr !== undefined) { + if (attr.type._class === core.class.EnumOf) { + const ref = (attr.type as EnumOf).of + const enu = modelDb.findAllSync(core.class.Enum, { _id: ref }) + res[key] = enu[0] + } + } + } + } + return res +} + +function getValue (key: string, obj: any, _class: Ref>, hierarchy: Hierarchy): any { + const tkey = checkMixinKey(key, _class, hierarchy) + let value = getObjectValue(tkey, obj) + if (typeof value === 'object' && !Array.isArray(value)) { + value = JSON.stringify(value) + } + return value +} +/** + * @public + */ +export function matchQuery ( + docs: Doc[], + query: DocumentQuery, + clazz: Ref>, + hierarchy: Hierarchy, + skipLookup: boolean = false +): Doc[] { + const baseClass = hierarchy.getBaseClass(clazz) + let result = docs.filter((r) => hierarchy.isDerived(r._class, baseClass)) + if (baseClass !== clazz) { + result = docs.filter((r) => hierarchy.hasMixin(r, clazz)) + } + for (const key in query) { + if (skipLookup && key.startsWith('$lookup.')) { + continue + } + const value = (query as any)[key] + const tkey = checkMixinKey(key, clazz, hierarchy) + result = findProperty(result, tkey, value) + if (result.length === 0) { + break + } + } + return result +} + +/** + * @public + */ +export function checkMixinKey (key: string, clazz: Ref>, hierarchy: Hierarchy): string { + if (!key.includes('.')) { + try { + const attr = hierarchy.findAttribute(clazz, key) + if (attr !== undefined && hierarchy.isMixin(attr.attributeOf)) { + // It is mixin + key = attr.attributeOf + '.' + key + } + } catch (err: any) { + // ignore, if + } + } + return key +} diff --git a/foundations/core/packages/core/src/server.ts b/foundations/core/packages/core/src/server.ts new file mode 100644 index 0000000000..a1515008ed --- /dev/null +++ b/foundations/core/packages/core/src/server.ts @@ -0,0 +1,137 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Account, AccountRole, AccountUuid, Doc, Domain, PersonId, Ref } from './classes' +import { type MeasureContext } from '@hcengineering/measurements' +import { type DocumentQuery, type FindOptions } from './storage' +import type { DocumentUpdate, Tx } from './tx' +import { PermissionsGrant, type WorkspaceIds } from './utils' + +/** + * @public + */ +export interface DocInfo { + id: string + hash: string + + size?: number + + contentType?: string +} +/** + * @public + */ +export interface StorageIterator { + next: (ctx: MeasureContext) => Promise + close: (ctx: MeasureContext) => Promise +} + +export interface BroadcastTargetResult { + target: AccountUuid[] +} + +export interface BroadcastExcludeResult { + exclude: AccountUuid[] +} + +export type BroadcastResult = BroadcastTargetResult | BroadcastExcludeResult | undefined +export type BroadcastTargets = Record Promise> + +export interface SessionData { + broadcast: { + txes: Tx[] + targets: BroadcastTargets // A set of broadcast filters if required + queue: Tx[] // Queue only broadcast + sessions: Record // Session based broadcast + } + contextCache: Map + removedMap: Map, Doc> + account: Account + service: string + sessionId: string + admin?: boolean + isTriggerCtx?: boolean + hasDomainBroadcast?: boolean + workspace: WorkspaceIds + socialStringsToUsers: Map< + PersonId, + { + accontUuid: AccountUuid + role: AccountRole + } + > + grant?: PermissionsGrant + + asyncRequests?: ((ctx: MeasureContext, id?: string) => Promise)[] +} + +/** + * @public + */ +export interface LowLevelStorage { + // Low level streaming API to retrieve information + find: (ctx: MeasureContext, domain: Domain) => StorageIterator + + // Load passed documents from domain + load: (ctx: MeasureContext, domain: Domain, docs: Ref[]) => Promise + + // Upload new versions of documents + // docs - new/updated version of documents. + upload: (ctx: MeasureContext, domain: Domain, docs: Doc[]) => Promise + + // Remove a list of documents. + clean: (ctx: MeasureContext, domain: Domain, docs: Ref[]) => Promise + + // Low level direct group API + groupBy: ( + ctx: MeasureContext, + domain: Domain, + field: string, + query?: DocumentQuery

+ ) => Promise> + + // migrations + rawFindAll: (domain: Domain, query: DocumentQuery, options?: FindOptions) => Promise + + rawUpdate: (domain: Domain, query: DocumentQuery, operations: DocumentUpdate) => Promise + + rawDeleteMany: (domain: Domain, query: DocumentQuery) => Promise + + // Traverse documents + traverse: ( + domain: Domain, + query: DocumentQuery, + options?: Pick, 'sort' | 'limit' | 'projection'> + ) => Promise> + + getDomainHash: (ctx: MeasureContext, domain: Domain) => Promise +} + +export interface Iterator { + next: (count: number) => Promise + close: () => Promise +} + +export interface Branding { + key?: string + front?: string + title?: string + language?: string + initWorkspace?: string + lastNameFirst?: string + protocol?: string +} + +export type BrandingMap = Record diff --git a/foundations/core/packages/core/src/status.ts b/foundations/core/packages/core/src/status.ts new file mode 100644 index 0000000000..94937e1f6f --- /dev/null +++ b/foundations/core/packages/core/src/status.ts @@ -0,0 +1,53 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Asset, type IntlString } from '@hcengineering/platform' +import { type Attribute, type Doc, type Domain, type Ref } from './classes' + +/** + * @public + */ +export interface StatusCategory extends Doc { + ofAttribute: Ref> + icon: Asset + label: IntlString + color: number | number[] + defaultStatusName: string + order: number // category order +} +/** + * @public + */ +export const DOMAIN_STATUS = 'status' as Domain + +/** + * @public + * + * Status is attached to attribute, and if user attribute will be removed, all status values will be remove as well. + */ +export interface Status extends Doc { + // We attach to attribute, so we could distinguish between + ofAttribute: Ref> + // Optional category. + category?: Ref + + // Status with case insensitivity name match will be assumed same. + name: string + + // Optional color + color?: number | number[] + // Optional description + description?: string +} diff --git a/foundations/core/packages/core/src/storage.ts b/foundations/core/packages/core/src/storage.ts new file mode 100644 index 0000000000..a97a84b076 --- /dev/null +++ b/foundations/core/packages/core/src/storage.ts @@ -0,0 +1,317 @@ +// +// Copyright © 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Asset, Resource } from '@hcengineering/platform' + +import type { Association, AttachedDoc, Class, Doc, Domain, Ref, Space } from './classes' +import type { Tx } from './tx' +import type { KeysByType } from './utils' + +export type ArraySizeSelector = + | { + $gt: number + } + | { + $lt: number + } + | { + $gte: number + } + | { + $lte: number + } + +/** + * @public + */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type QuerySelector = { + $in?: T[] + $all?: T extends Array ? T : never + $nin?: T[] + $ne?: T + $gt?: T extends number ? number : never + $gte?: T extends number ? number : never + $lt?: T extends number ? number : never + $lte?: T extends number ? number : never + $exists?: boolean + $like?: string + $regex?: string + $options?: string + $size?: T extends Array ? number | ArraySizeSelector : never +} + +/** + * @public + */ +export type ObjQueryType = (T extends Array ? U | U[] | QuerySelector : T) | QuerySelector + +/** + * @public + */ +export type DocumentQuery = { + [P in keyof T]?: ObjQueryType +} & { + $search?: string + // support nested queries e.g. 'user.friends.name' + // this will mark all unrecognized properties as any (including nested queries) + [key: string]: any +} + +/** + * @public + */ +export type ToClassRefT = T[P] extends Ref | null | undefined + ? Ref> | [Ref>, Lookup] + : never +/** + * @public + */ +export type ToClassRefTA = T[P] extends Array> | null | undefined + ? Ref> | [Ref>, Lookup] + : never +/** + * @public + */ +export type ToClassRef = { + [P in keyof T]?: ToClassRefT | ToClassRefTA +} + +/** + * @public + */ +export type NullableRef = Ref | Array> | null | undefined + +/** + * @public + */ +export type RefKeys = Pick> + +/** + * @public + */ +export type Refs = ToClassRef> + +/** + * @public + */ +export interface ReverseLookups { + _id?: ReverseLookup +} + +/** + * @public + */ +export type ReverseLookup = Record> | [Ref>, string]> + +/** + * @public + */ +export type Lookup = Refs | ReverseLookups | (Refs & ReverseLookups) + +/** + * @public + */ +export type Projection = { + [P in keyof T]?: 0 | 1 +} + +export type AssociationQuery = [Ref, 1 | -1] + +/** + * @public + */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type FindOptions = { + limit?: number + sort?: SortingQuery + lookup?: Lookup + projection?: Projection + associations?: AssociationQuery[] + + // If specified total will be returned + total?: boolean + + showArchived?: boolean +} + +/** + * @public + */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type SortQuerySelector = { + $in?: T[] + $nin?: T[] + $ne?: T +} +/** + * @public + */ +export type SortRuleQueryType = (T extends Array ? U | U[] : T) | SortQuerySelector + +/** + * @public + */ +export interface SortingRules { + order: SortingOrder + default?: string | number + cases: { + query: SortRuleQueryType + index: string | number + }[] +} + +/** + * @public + */ +export type SortingQuery = { + [P in keyof T]?: SortingOrder | SortingRules +} & Record> + +/** + * @public + */ +export enum SortingOrder { + Ascending = 1, + Descending = -1 +} + +/** + * @public + */ +export type RefsAsDocs = { + [P in keyof T]: T[P] extends Ref | null | undefined ? (T extends X ? X : X | WithLookup) : AttachedDoc[] +} + +/** + * @public + */ +export type LookupData = Partial> + +/** + * @public + */ +export type WithLookup = T & { + $lookup?: LookupData + $associations?: Record + $source?: { + $score: number // Score for document result + [key: string]: any + } +} + +/** + * @public + */ +export type FindResult = WithLookup[] & { + total: number + lookupMap?: Record +} + +export type DomainParams = Record + +export interface DomainResult { + domain: Domain + value: T +} + +/** + * @public + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TxResult {} + +/** + * @public + */ +export interface SearchQuery { + query: string + classes?: Ref>[] + spaces?: Ref[] +} + +/** + * @public + */ +export interface SearchOptions { + limit?: number +} + +export interface SearchComponentWithProps { + component?: Resource + props?: Record +} + +/** + * @public + */ +export interface SearchResultDoc { + id: Ref + + icon?: Asset + iconComponent?: SearchComponentWithProps + shortTitle?: string + shortTitleComponent?: SearchComponentWithProps + title?: string + titleComponent?: SearchComponentWithProps + description?: string + emojiIcon?: string + score?: number + doc: Pick & Partial> +} + +/** + * @public + */ +export interface SearchResult { + docs: SearchResultDoc[] + total?: number +} + +/** + * @public + */ +export interface Storage { + findAll: ( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ) => Promise> + + tx: (tx: Tx) => Promise +} + +/** + * @public + */ +export interface FulltextStorage { + searchFulltext: (query: SearchQuery, options: SearchOptions) => Promise +} + +export function shouldShowArchived ( + query: DocumentQuery, + options: FindOptions | undefined +): boolean { + if (options?.showArchived !== undefined) { + return options.showArchived + } + if (query._id !== undefined && typeof query._id === 'string') { + return true + } + if (query.space !== undefined && typeof query.space === 'string') { + return true + } + return false +} diff --git a/foundations/core/packages/core/src/time.ts b/foundations/core/packages/core/src/time.ts new file mode 100644 index 0000000000..52569d8eca --- /dev/null +++ b/foundations/core/packages/core/src/time.ts @@ -0,0 +1,68 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { type Timestamp } from './classes' + +export function getDay (time: Timestamp): Timestamp { + const date: Date = new Date(time) + return convertToDay(date).getTime() +} + +export function convertToDay (date: Date): Date { + const originalDay: number = date.getDate() + const convertedDate: Date = new Date(date) + // Set 12 AM UTC time, since it will be the same day in most timezones + convertedDate.setUTCHours(12, 0, 0, 0) + if (convertedDate.getDate() !== originalDay) { + convertedDate.setDate(originalDay) + } + return convertedDate +} + +export function getHour (time: Timestamp): Timestamp { + const date: Date = new Date(time) + date.setMinutes(0, 0, 0) + return date.getTime() +} + +export function getDisplayTime (time: number): string { + let options: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric' } + if (!isToday(time)) { + options = { + month: 'numeric', + day: 'numeric', + ...options + } + } + + return new Date(time).toLocaleString('default', options) +} + +export function isOtherDay (time1: Timestamp, time2: Timestamp): boolean { + return getDay(time1) !== getDay(time2) +} + +export function isOtherHour (time1: Timestamp, time2: Timestamp): boolean { + return getHour(time1) !== getHour(time2) +} + +function isToday (time: number): boolean { + const current = new Date() + const target = new Date(time) + return ( + current.getDate() === target.getDate() && + current.getMonth() === target.getMonth() && + current.getFullYear() === target.getFullYear() + ) +} diff --git a/foundations/core/packages/core/src/tx.ts b/foundations/core/packages/core/src/tx.ts new file mode 100644 index 0000000000..af3cea4f17 --- /dev/null +++ b/foundations/core/packages/core/src/tx.ts @@ -0,0 +1,606 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { + Arr, + AttachedDoc, + Class, + Data, + Doc, + Domain, + Mixin, + OperationDomain, + PersonId, + PropertyType, + Ref, + Space, + Timestamp +} from './classes' +import { clone } from './clone' +import core from './component' +import { setObjectValue } from './objvalue' +import { _getOperator } from './operator' +import { _toDoc } from './proxy' +import type { DocumentQuery, TxResult } from './storage' +import { generateId, type KeysByType } from './utils' + +/** + * @public + */ +export interface Tx extends Doc { + objectSpace: Ref // space where transaction will operate + meta?: Record // meta information about transaction, non persisted to final DB's +} + +/** + * @public + */ +export enum WorkspaceEvent { + UpgradeScheduled, + IndexingUpdate, + SecurityChange, + MaintenanceNotification, + BulkUpdate, + LastTx +} + +/** + * Event to be send by server during model upgrade procedure. + * @public + */ +export interface TxWorkspaceEvent extends Tx { + event: WorkspaceEvent + params: T +} +export interface TxDomainEvent extends Tx { + domain: OperationDomain + event: T +} + +/** + * @public + */ +export interface IndexingUpdateEvent { + _class: Ref>[] +} + +/** + * @public + */ +export interface BulkUpdateEvent { + _class: Ref>[] +} + +/** + * @public + */ +export interface TxModelUpgrade extends Tx {} + +/** + * @public + */ +export interface TxCUD extends Tx { + objectId: Ref + objectClass: Ref> + attachedTo?: Ref + attachedToClass?: Ref> + collection?: string +} + +/** + * @public + */ +export interface TxCreateDoc extends TxCUD { + attributes: Data +} + +/** + * @public + */ +export interface DocumentClassQuery { + _class: Ref> + query: DocumentQuery +} + +/** + * @public + * Apply set of transactions in sequential manner with verification of set of queries. + */ +export interface TxApplyIf extends Tx { + // only one operation per scope is allowed at one time. + scope?: string + + // All matches should be true with at least one document. + match?: DocumentClassQuery[] + + // All matches should be false for all documents. + notMatch?: DocumentClassQuery[] + + // If all matched execute following transactions. + txes: TxCUD[] + + notify?: boolean // If false will not send notifications. + + // If passed, will send WorkspaceEvent.BulkUpdate event with list of classes to update + extraNotify?: Ref>[] + + // If defined will go into a separate measure section + measureName?: string +} + +export interface TxApplyResult { + success: boolean + serverTime: number +} + +/** + * @public + */ +export type MixinData = Omit & +PushOptions> & +IncOptions> + +/** + * @public + */ +export type MixinUpdate = Partial> & +PushOptions> & +IncOptions> + +/** + * Define Create/Update for mixin attributes. + * @public + */ +export interface TxMixin extends TxCUD { + mixin: Ref> + attributes: MixinUpdate +} + +/** + * @public + */ +export type ArrayAsElement = { + [P in keyof T]: T[P] extends Arr ? Partial | PullArray | X : never +} + +/** + * @public + */ +export interface Position { + $each: X[] + $position: number +} + +/** + * @public + */ +export interface QueryUpdate { + $query: Partial + $update: Partial +} + +/** + * @public + */ +export interface PullArray { + $in: X[] +} + +/** + * @public + */ +export interface MoveDescriptor { + $value: X + $position: number +} + +/** + * @public + */ +export type ArrayAsElementPosition = { + [P in keyof T]-?: T[P] extends Arr ? X | Position : never +} + +/** + * @public + */ +export type ArrayAsElementUpdate = { + [P in keyof T]-?: T[P] extends Arr ? X | QueryUpdate : never +} + +/** + * @public + */ +export type ArrayMoveDescriptor = { + [P in keyof T]: T[P] extends Arr ? MoveDescriptor : never +} + +/** + * @public + */ +export type NumberProperties = { + [P in keyof T]: T[P] extends number | undefined | null ? T[P] : never +} + +/** + * @public + */ +export type OmitNever = Omit> + +/** + * @public + */ +export interface PushOptions { + $push?: Partial>>> + $pull?: Partial>>> +} + +/** + * @public + */ +export type UnsetProperties = Record + +/** + * @public + */ +export interface UnsetOptions { + $unset?: UnsetProperties +} + +/** + * @public + */ +export interface SetEmbeddedOptions { + $update?: Partial>>> +} + +/** + * @public + */ +export interface IncOptions { + $inc?: Partial>> +} + +/** + * @public + */ +export interface SpaceUpdate { + space?: Ref +} + +/** + * @public + */ +export type DocumentUpdate = Partial> & +PushOptions & +SetEmbeddedOptions & +IncOptions & +UnsetOptions & +SpaceUpdate + +/** + * @public + */ +export interface TxUpdateDoc extends TxCUD { + operations: DocumentUpdate + retrieve?: boolean +} + +/** + * @public + */ +export interface TxRemoveDoc extends TxCUD {} + +/** + * @public + */ +export const DOMAIN_TX = 'tx' as Domain + +/** + * @public + */ +export interface WithTx { + tx: (...txs: Tx[]) => Promise +} + +/** + * @public + */ +export abstract class TxProcessor implements WithTx { + async tx (...txes: Tx[]): Promise { + const result: TxResult[] = [] + for (const tx of txes) { + switch (tx._class) { + case core.class.TxCreateDoc: + result.push(await this.txCreateDoc(tx as TxCreateDoc)) + break + case core.class.TxUpdateDoc: + result.push(await this.txUpdateDoc(tx as TxUpdateDoc)) + break + case core.class.TxRemoveDoc: + result.push(await this.txRemoveDoc(tx as TxRemoveDoc)) + break + case core.class.TxMixin: + result.push(await this.txMixin(tx as TxMixin)) + break + case core.class.TxApplyIf: + // Apply if processed on server + return await Promise.resolve([]) + } + } + return result + } + + static createDoc2Doc(tx: TxCreateDoc, doClone = true): T { + const attached = + tx.attachedTo !== undefined + ? { + attachedTo: tx.attachedTo, + attachedToClass: tx.attachedToClass, + collection: tx.collection + } + : {} + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + ...(doClone ? clone(tx.attributes) : tx.attributes), + ...attached, + _id: tx.objectId, + _class: tx.objectClass, + space: tx.objectSpace, + modifiedBy: tx.modifiedBy, + modifiedOn: tx.modifiedOn, + createdBy: tx.createdBy ?? tx.modifiedBy, + createdOn: tx.createdOn ?? tx.modifiedOn + } as T + } + + static updateDoc2Doc(rawDoc: T, tx: TxUpdateDoc): T { + const doc = _toDoc(rawDoc) + TxProcessor.applyUpdate(doc, tx.operations as any) + doc.modifiedBy = tx.modifiedBy + doc.modifiedOn = tx.modifiedOn + return rawDoc + } + + static applyUpdate(doc: T, ops: any): void { + for (const key in ops) { + if (key.startsWith('$')) { + const operator = _getOperator(key) + operator(doc, ops[key]) + } else { + setObjectValue(key, doc, ops[key]) + } + } + } + + static updateMixin4Doc(rawDoc: D, tx: TxMixin): D { + const ops = tx.attributes as any + const doc = _toDoc(rawDoc) + const mixin = (doc as any)[tx.mixin] ?? {} + for (const key in ops) { + if (key.startsWith('$')) { + const operator = _getOperator(key) + operator(mixin, ops[key]) + } else { + setObjectValue(key, mixin, ops[key]) + } + } + rawDoc.modifiedBy = tx.modifiedBy + rawDoc.modifiedOn = tx.modifiedOn + ;(doc as any)[tx.mixin] = mixin + return rawDoc + } + + static buildDoc2Doc(txes: Tx[]): D | undefined | null { + let doc: Doc + const deleteTx = txes.find((tx) => tx._class === core.class.TxRemoveDoc) + if (deleteTx !== undefined) { + return null + } + const createTx = txes.find((tx) => tx._class === core.class.TxCreateDoc) + if (createTx === undefined) { + return + } + doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc) + for (const tx of txes) { + if (tx._class === core.class.TxUpdateDoc) { + doc = TxProcessor.updateDoc2Doc(doc, tx as TxUpdateDoc) + } else if (tx._class === core.class.TxMixin) { + const mixinTx = tx as TxMixin + doc = TxProcessor.updateMixin4Doc(doc, mixinTx) + } + } + return doc as D + } + + static isExtendsCUD (_class: Ref>): boolean { + return ( + _class === core.class.TxCreateDoc || + _class === core.class.TxUpdateDoc || + _class === core.class.TxRemoveDoc || + _class === core.class.TxMixin + ) + } + + static txHasUpdate(tx: TxUpdateDoc, attribute: string): boolean { + const ops = tx.operations + if ((ops as any)[attribute] !== undefined) return true + for (const op in ops) { + if (op.startsWith('$')) { + const opValue = (ops as any)[op] + for (const key in opValue) { + if (key === attribute || key.startsWith(attribute + '.')) { + return true + } + } + } + } + return false + } + + protected abstract txCreateDoc (tx: TxCreateDoc): Promise + protected abstract txUpdateDoc (tx: TxUpdateDoc): Promise + protected abstract txRemoveDoc (tx: TxRemoveDoc): Promise + protected abstract txMixin (tx: TxMixin): Promise +} + +/** + * @public + */ +export class TxFactory { + private readonly txSpace: Ref + constructor ( + readonly account: PersonId, + readonly isDerived: boolean = false + ) { + this.txSpace = isDerived ? core.space.DerivedTx : core.space.Tx + } + + createTxCreateDoc( + _class: Ref>, + space: Ref, + attributes: Data, + objectId?: Ref, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): TxCreateDoc { + return { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: this.txSpace, + objectId: objectId ?? generateId(), + objectClass: _class, + objectSpace: space, + modifiedOn: modifiedOn ?? Date.now(), + modifiedBy: modifiedBy ?? this.account, + createdBy: modifiedBy ?? this.account, + attributes + } + } + + createTxCollectionCUD( + _class: Ref>, + objectId: Ref, + space: Ref, + collection: string, + tx: TxCUD

, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): TxCUD

{ + return { + ...tx, + collection, + attachedTo: objectId, + attachedToClass: _class, + modifiedOn: modifiedOn ?? Date.now(), + modifiedBy: modifiedBy ?? this.account + } + } + + createTxUpdateDoc( + _class: Ref>, + space: Ref, + objectId: Ref, + operations: DocumentUpdate, + retrieve?: boolean, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): TxUpdateDoc { + return { + _id: generateId(), + _class: core.class.TxUpdateDoc, + space: this.txSpace, + modifiedBy: modifiedBy ?? this.account, + modifiedOn: modifiedOn ?? Date.now(), + objectId, + objectClass: _class, + objectSpace: space, + operations, + retrieve + } + } + + createTxRemoveDoc( + _class: Ref>, + space: Ref, + objectId: Ref, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): TxRemoveDoc { + return { + _id: generateId(), + _class: core.class.TxRemoveDoc, + space: this.txSpace, + modifiedBy: modifiedBy ?? this.account, + modifiedOn: modifiedOn ?? Date.now(), + objectId, + objectClass: _class, + objectSpace: space + } + } + + createTxMixin( + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + mixin: Ref>, + attributes: MixinUpdate, + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): TxMixin { + return { + _id: generateId(), + _class: core.class.TxMixin, + space: this.txSpace, + modifiedBy: modifiedBy ?? this.account, + modifiedOn: modifiedOn ?? Date.now(), + objectId, + objectClass, + objectSpace, + mixin, + attributes + } + } + + createTxApplyIf ( + space: Ref, + scope: string | undefined, + match: DocumentClassQuery[], + notMatch: DocumentClassQuery[], + txes: TxCUD[], + measureName: string | undefined, + notify: boolean = true, + extraNotify: Ref>[] = [], + modifiedOn?: Timestamp, + modifiedBy?: PersonId + ): TxApplyIf { + return { + _id: generateId(), + _class: core.class.TxApplyIf, + space: this.txSpace, + modifiedBy: modifiedBy ?? this.account, + modifiedOn: modifiedOn ?? Date.now(), + objectSpace: space, + scope, + match, + notMatch, + txes, + measureName, + notify, + extraNotify + } + } +} diff --git a/foundations/core/packages/core/src/utils.ts b/foundations/core/packages/core/src/utils.ts new file mode 100644 index 0000000000..dddc0c0706 --- /dev/null +++ b/foundations/core/packages/core/src/utils.ts @@ -0,0 +1,1010 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { getEmbeddedLabel, getMetadata, type IntlString } from '@hcengineering/platform' +import { deepEqual } from 'fast-equals' +import { DOMAIN_BENCHMARK } from './benchmark' +import { + type Account, + AccountRole, + type AccountUuid, + type AnyAttribute, + type AttachedDoc, + type Class, + ClassifierKind, + type Collection, + type Doc, + type DocData, + type Domain, + DOMAIN_BLOB, + DOMAIN_MODEL, + DOMAIN_TRANSIENT, + type FullTextSearchContext, + IndexKind, + type Obj, + type Permission, + type PluginConfiguration, + type Rank, + type Ref, + type Role, + roleOrder, + type SocialId, + SocialIdType, + type SocialKey, + type Space, + type TypedSpace, + type WorkspaceMode +} from './classes' +import core from './component' +import { type Hierarchy } from './hierarchy' +import { type TxOperations } from './operations' +import { isPredicate } from './predicate' +import { type Branding, type BrandingMap } from './server' +import { type DocumentQuery, type FindResult } from './storage' +import { DOMAIN_TX, type Tx, type TxCreateDoc, type TxCUD, TxProcessor, type TxUpdateDoc } from './tx' + +function toHex (value: number, chars: number): string { + const result = value.toString(16) + if (result.length < chars) { + return '0'.repeat(chars - result.length) + result + } + return result +} + +let counter = (Math.random() * (1 << 24)) | 0 +const random = toHex((Math.random() * (1 << 24)) | 0, 6) + toHex((Math.random() * (1 << 16)) | 0, 4) + +function timestamp (): string { + const time = (Date.now() / 1000) | 0 + return toHex(time, 8) +} + +function count (): string { + const val = counter++ & 0xffffff + return toHex(val, 6) +} + +/** + * @public + * @returns + */ +export function generateId (join: string = ''): Ref { + return (timestamp() + join + random + join + count()) as Ref +} + +export function generateUuid (): string { + // Consider own implementation if it will be slow + return crypto.randomUUID() +} + +/** @public */ +export function isId (value: any): value is Ref { + return typeof value === 'string' && /^[0-9a-f]{24,24}$/.test(value) +} + +let currentAccount: Account + +/** + * @public + * @returns + */ +export function getCurrentAccount (): Account { + return currentAccount +} + +/** + * @public + * @param account - + */ +export function setCurrentAccount (account: Account): void { + currentAccount = account +} +/** + * @public + */ +export function escapeLikeForRegexp (value: string): string { + return value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') +} + +/** + * @public + */ +export function toFindResult (docs: T[], total?: number, lookupMap?: Record): FindResult { + const length = total ?? docs.length + if (Object.keys(lookupMap ?? {}).length === 0) { + lookupMap = undefined + } + return Object.assign(docs, { total: length, lookupMap }) +} + +export type WorkspaceUuid = string & { __workspaceUuid: true } +export type WorkspaceDataId = string & { __workspaceDataId: true } +export interface WorkspaceIds { + uuid: WorkspaceUuid + url: string + dataId?: WorkspaceDataId // Old workspace identifier. E.g. Database name in Mongo, bucket in R2, etc. +} + +/** + * @public + */ +export function isWorkspaceCreating (mode?: WorkspaceMode): boolean { + if (mode === undefined) { + return false + } + + return ['pending-creation', 'creating'].includes(mode) +} + +/** + * @public + */ +export function docKey (name: string, _class?: Ref>): string { + return _class === undefined || _class !== core.class.Doc ? name : `${_class}%${name}` +} + +/** + * @public + */ +export function isFullTextAttribute (attr: AnyAttribute): boolean { + return ( + attr.index === IndexKind.FullText || + attr.type._class === core.class.TypeBlob || + attr.type._class === core.class.EnumOf || + attr.type._class === core.class.TypeCollaborativeDoc + ) +} + +/** + * @public + */ +export function isIndexedAttribute (attr: AnyAttribute): boolean { + return attr.index === IndexKind.Indexed || attr.index === IndexKind.IndexedDsc +} + +/** + * @public + */ +export interface IdMap extends Map, T> {} + +/** + * @public + */ +export function toIdMap (arr: T[]): IdMap { + return new Map(arr.map((p) => [p._id, p])) +} + +/** + * @public + */ +export function concatLink (host: string, path: string): string { + if (!host.endsWith('/') && !path.startsWith('/')) { + return `${host}/${path}` + } else if (host.endsWith('/') && path.startsWith('/')) { + const newPath = path.slice(1) + return `${host}${newPath}` + } else { + return `${host}${path}` + } +} + +/** + * @public + */ +export function fillDefaults ( + hierarchy: Hierarchy, + object: DocData | T, + _class: Ref> +): DocData | T { + const baseClass = hierarchy.isDerived(_class, core.class.AttachedDoc) ? core.class.AttachedDoc : core.class.Doc + const attributes = hierarchy.getAllAttributes(_class, baseClass) + for (const attribute of attributes) { + if (attribute[1].defaultValue !== undefined) { + if ((object as any)[attribute[0]] === undefined) { + // Clone default value as it might be an object (e.g. array) + ;(object as any)[attribute[0]] = structuredClone(attribute[1].defaultValue) + } + } + } + return object +} + +/** + * @public + */ +export class AggregateValueData { + constructor ( + readonly name: string, + readonly _id: Ref, + readonly space: Ref, + readonly rank?: string, + readonly category?: Ref + ) {} + + getRank (): string { + return this.rank ?? '' + } +} + +/** + * @public + */ +export class AggregateValue { + constructor ( + readonly name: string | undefined, + readonly values: AggregateValueData[] + ) {} +} + +/** + * @public + */ +export type CategoryType = number | string | undefined | Ref | AggregateValue + +export interface IDocManager { + get: (ref: Ref) => T | undefined + getDocs: () => T[] + getIdMap: () => IdMap + filter: (predicate: (value: T) => boolean) => T[] +} + +/** + * @public + */ +export class DocManager implements IDocManager { + protected readonly byId: IdMap + + constructor (protected readonly docs: T[]) { + this.byId = toIdMap(docs) + } + + get (ref: Ref): T | undefined { + return this.byId.get(ref) + } + + getDocs (): T[] { + return this.docs + } + + getIdMap (): IdMap { + return this.byId + } + + filter (predicate: (value: T) => boolean): T[] { + return this.docs.filter(predicate) + } +} + +/** + * @public + */ + +export class RateLimiter { + idCounter: number = 0 + processingQueue = new Map>() + last: number = 0 + rate: number + + queue: (() => Promise)[] = [] + + constructor (rate: number) { + this.rate = rate + } + + notify: (() => void)[] = [] + + async exec = any>(op: (args?: B) => Promise, args?: B): Promise { + const processingId = this.idCounter++ + + while (this.processingQueue.size >= this.rate) { + await new Promise((resolve) => { + this.notify.push(resolve) + }) + } + try { + const p = op(args) + this.processingQueue.set(processingId, p as Promise) + return await p + } finally { + this.processingQueue.delete(processingId) + const n = this.notify.shift() + if (n !== undefined) { + n() + } + } + } + + async add = any>( + op: (args?: B) => Promise, + args?: B, + errHandler?: (err: any) => void + ): Promise { + while (this.processingQueue.size >= this.rate) { + await new Promise((resolve) => { + this.notify.push(resolve) + }) + } + void this.exec(op, args).catch((err) => { + if (errHandler !== undefined) { + errHandler(err) + } + console.error('Failed to execute in rate limitter', err) + }) + } + + async waitProcessing (): Promise { + while (this.processingQueue.size > 0) { + await new Promise((resolve) => { + this.notify.push(resolve) + }) + } + } +} + +export function mergeQueries (query1: DocumentQuery, query2: DocumentQuery): DocumentQuery { + const keys1 = Object.keys(query1) + const keys2 = Object.keys(query2) + + const query = {} + + for (const key of keys1) { + if (!keys2.includes(key)) { + Object.assign(query, { [key]: query1[key] }) + } + } + + for (const key of keys2) { + if (!keys1.includes(key)) { + Object.assign(query, { [key]: query2[key] }) + } else { + const value = mergeField(query1[key], query2[key]) + if (value !== undefined) { + Object.assign(query, { [key]: value }) + } + } + } + + return query +} + +function mergeField (field1: any, field2: any): any | undefined { + // this is a special predicate that causes query never return any docs + // it is used in cases when queries intersection is empty + const never = { $in: [] } + // list of ignored predicates, handled separately + const ignored = ['$in', '$nin', '$ne'] + + const isPredicate1 = isPredicate(field1) + const isPredicate2 = isPredicate(field2) + + if (isPredicate1 && isPredicate2) { + // $in, $nin, $eq are related fields so handle them separately here + const result = getInNiN(field1, field2) + + const keys1 = Object.keys(field1) + const keys2 = Object.keys(field2) + + for (const key of keys1) { + if (ignored.includes(key)) continue + + if (!keys2.includes(key)) { + Object.assign(result, { [key]: field1[key] }) + } else { + const value = mergePredicateWithPredicate(key, field1[key], field2[key]) + if (value !== undefined) { + Object.assign(result, { [key]: value }) + } + } + } + + for (const key of keys2) { + if (ignored.includes(key)) continue + + if (!keys1.includes(key)) { + Object.assign(result, { [key]: field2[key] }) + } + } + + return Object.keys(result).length > 0 ? result : undefined + } else if (isPredicate1 || isPredicate2) { + // when one field is a predicate and the other is a simple value + // we need to ensure that the value matches predicate + const predicate = isPredicate1 ? field1 : field2 + const value = isPredicate1 ? field2 : field1 + + for (const x in predicate) { + const result = mergePredicateWithValue(x, predicate[x], value) + if ( + Array.isArray(result?.$in) && + result.$in.length > 0 && + Array.isArray(result?.$nin) && + result.$nin.length === 0 + ) { + delete result.$nin + } + if (result !== undefined) { + return result + } + } + + // if we reached here, the value does not match the predicate + return never + } else { + // both are not predicates, can filter only when values are equal + return deepEqual(field1, field2) ? field1 : never + } +} + +function mergePredicateWithPredicate (predicate: string, val1: any, val2: any): any | undefined { + if (val1 === undefined) return val2 + if (val2 === undefined) return val1 + + switch (predicate) { + case '$lt': + return val1 < val2 ? val1 : val2 + case '$lte': + return val1 <= val2 ? val1 : val2 + case '$gt': + return val1 > val2 ? val1 : val2 + case '$gte': + return val1 >= val2 ? val1 : val2 + } + + // TODO we should properly support all available predicates here + // until then, fallback to the first predicate value + + return val1 +} + +function mergePredicateWithValue (predicate: string, val1: any, val2: any): any | undefined { + switch (predicate) { + case '$in': + return Array.isArray(val1) && val1.includes(val2) ? val2 : undefined + case '$nin': + return Array.isArray(val1) && !val1.includes(val2) ? val2 : undefined + case '$lt': + return val2 < val1 ? val2 : undefined + case '$lte': + return val2 <= val1 ? val2 : undefined + case '$gt': + return val2 > val1 ? val2 : undefined + case '$gte': + return val2 >= val1 ? val2 : undefined + case '$ne': + return val1 !== val2 ? val2 : undefined + } + + // TODO we should properly support all available predicates here + // until then, fallback to the non-predicate value + + return val2 +} + +function getInNiN (query1: any, query2: any): any { + const aIn = typeof query1 === 'object' && '$in' in query1 ? query1.$in : undefined + const bIn = typeof query2 === 'object' && '$in' in query2 ? query2.$in : undefined + const aNIn = + (typeof query1 === 'object' && '$nin' in query1 ? query1.$nin : undefined) ?? + (typeof query1 === 'object' && query1.$ne !== undefined ? [query1.$ne] : []) + const bNIn = + (typeof query2 === 'object' && '$nin' in query2 ? query2.$nin : undefined) ?? + (typeof query1 === 'object' && query2.$ne !== undefined ? [query2.$ne] : []) + + const finalNin = Array.from(new Set([...aNIn, ...bNIn])) + + // we must keep $in if it was in the original query + if (aIn !== undefined || bIn !== undefined) { + const finalIn = + aIn !== undefined && bIn !== undefined + ? aIn.length - bIn.length < 0 + ? bIn.filter((c: any) => aIn.includes(c)) + : aIn.filter((c: any) => bIn.includes(c)) + : (aIn ?? bIn) + return { $in: finalIn.filter((p: any) => !finalNin.includes(p)) } + } + // try to preserve original $ne instead of $nin + if ((typeof query1 === 'object' && '$ne' in query1) || (typeof query2 === 'object' && '$ne' in query2)) { + if (finalNin.length === 1) { + return { $ne: finalNin[0] } + } + } + if (finalNin.length > 0) { + return { $nin: finalNin } + } + return {} +} + +export function cutObjectArray (obj: any): any { + if (obj == null) { + return obj + } + const r = {} + for (const key of Object.keys(obj)) { + if (Array.isArray(obj[key])) { + if (obj[key].length > 3) { + Object.assign(r, { [key]: [...obj[key].slice(0, 3), `... and ${obj[key].length - 3} more`] }) + } else Object.assign(r, { [key]: obj[key] }) + continue + } + if (typeof obj[key] === 'object' && obj[key] !== null) { + Object.assign(r, { [key]: cutObjectArray(obj[key]) }) + continue + } + Object.assign(r, { [key]: obj[key] }) + } + return r +} + +export function includesAny (arr1: string[] | null | undefined, arr2: string[] | null | undefined): boolean { + if (arr1 == null || arr1.length === 0 || arr2 == null || arr2.length === 0) { + return false + } + + return arr1.some((m) => arr2.includes(m)) +} + +export const isEnum = + (e: T) => + (token: any): token is T[keyof T] => { + return typeof token === 'string' && Object.values(e as Record).includes(token) + } + +export async function checkPermission ( + client: TxOperations, + _id: Ref, + _space: Ref, + space?: TypedSpace +): Promise { + const arePermissionsDisabled = getMetadata(core.metadata.DisablePermissions) ?? false + if (arePermissionsDisabled) return true + + return await hasPermission(client, _id, _space, space) +} + +export async function checkForbiddenPermission ( + client: TxOperations, + _id: Ref, + _space: Ref, + space?: TypedSpace +): Promise { + const arePermissionsDisabled = getMetadata(core.metadata.DisablePermissions) ?? false + if (arePermissionsDisabled) return false + + return await hasPermission(client, _id, _space, space) +} + +async function hasPermission ( + client: TxOperations, + _id: Ref, + _space: Ref, + space?: TypedSpace +): Promise { + space = space ?? (await client.findOne(core.class.TypedSpace, { _id: _space })) + const type = await client + .getModel() + .findOne(core.class.SpaceType, { _id: space?.type }, { lookup: { _id: { roles: core.class.Role } } }) + const mixin = type?.targetClass + if (space === undefined || type === undefined || mixin === undefined) { + return false + } + + const me = getCurrentAccount() + const asMixin = client.getHierarchy().as(space, mixin) + const myRoles = type.$lookup?.roles?.filter((role) => ((asMixin as any)[role._id] ?? []).includes(me.uuid)) as Role[] + + if (myRoles === undefined) { + return false + } + + const myPermissions = new Set(myRoles.flatMap((role) => role.permissions)) + + return myPermissions.has(_id) +} + +/** + * @public + */ +export function getRoleAttributeLabel (roleName: string): IntlString { + return getEmbeddedLabel(`Role: ${roleName.trim()}`) +} + +/** + * @public + */ +export function getFullTextIndexableAttributes ( + hierarchy: Hierarchy, + clazz: Ref>, + skipDocs: boolean = false +): AnyAttribute[] { + const allAttributes = hierarchy.getAllAttributes(clazz) + const result: AnyAttribute[] = [] + for (const [, attr] of allAttributes) { + if (skipDocs && (attr.attributeOf === core.class.Doc || attr.attributeOf === core.class.AttachedDoc)) { + continue + } + if (isFullTextAttribute(attr) || isIndexedAttribute(attr)) { + result.push(attr) + } + } + + hierarchy + .getDescendants(clazz) + .filter((m) => hierarchy.getClass(m).kind === ClassifierKind.MIXIN) + .forEach((m) => { + for (const [, v] of hierarchy.getAllAttributes(m, clazz)) { + if (skipDocs && (v.attributeOf === core.class.Doc || v.attributeOf === core.class.AttachedDoc)) { + continue + } + if (isFullTextAttribute(v) || isIndexedAttribute(v)) { + result.push(v) + } + } + }) + return result +} + +const ctxKey = 'indexer_ftc' +/** + * @public + */ +export function getFullTextContext ( + hierarchy: Hierarchy, + objectClass: Ref>, + contexts: Map>, FullTextSearchContext> +): Omit> { + let ctx: Omit> | undefined = hierarchy.getClassifierProp(objectClass, ctxKey) + if (ctx !== undefined) { + return ctx + } + if (typeof ctx !== 'string') { + const anc = hierarchy.getAncestors(objectClass) + for (const oc of anc) { + const ctx = contexts.get(oc) + if (ctx !== undefined) { + hierarchy.setClassifierProp(objectClass, ctxKey, ctx) + return ctx + } + } + } + ctx = { + toClass: objectClass, + fullTextSummary: false, + forceIndex: false + } + hierarchy.setClassifierProp(objectClass, ctxKey, ctx) + return ctx +} + +/** + * @public + */ +export function isClassIndexable ( + hierarchy: Hierarchy, + c: Ref>, + contexts: Map>, FullTextSearchContext> +): boolean { + const indexed = hierarchy.getClassifierProp(c, 'class_indexed') + if (indexed !== undefined) { + return indexed as boolean + } + const domain = hierarchy.findDomain(c) + if (domain === undefined) { + hierarchy.setClassifierProp(c, 'class_indexed', false) + return false + } + + if ( + domain === DOMAIN_TX || + domain === DOMAIN_MODEL || + domain === DOMAIN_BLOB || + domain === ('preference' as Domain) || + domain === DOMAIN_TRANSIENT || + domain === ('settings' as Domain) || + domain === DOMAIN_BENCHMARK + ) { + hierarchy.setClassifierProp(c, 'class_indexed', false) + return false + } + + const indexMixin = hierarchy.classHierarchyMixin(c, core.mixin.IndexConfiguration) + if (indexMixin?.searchDisabled !== undefined && indexMixin?.searchDisabled) { + hierarchy.setClassifierProp(c, 'class_indexed', false) + return false + } + + const attrs = getFullTextIndexableAttributes(hierarchy, c, true) + for (const d of hierarchy.getDescendants(c)) { + if (hierarchy.isMixin(d)) { + attrs.push(...getFullTextIndexableAttributes(hierarchy, d, true)) + } + } + + let result = true + + if (attrs.length === 0 && !(getFullTextContext(hierarchy, c, contexts)?.forceIndex ?? false)) { + result = false + // We need check if document has collections with indexable fields. + const attrs = hierarchy.getAllAttributes(c).values() + for (const attr of attrs) { + if (attr.type._class === core.class.Collection) { + if (isClassIndexable(hierarchy, (attr.type as Collection).of, contexts)) { + result = true + break + } + } + } + } + hierarchy.setClassifierProp(c, 'class_indexed', result) + return result +} + +type ReduceParameters any> = T extends (...args: infer P) => any ? P : never + +interface NextCall { + op: () => Promise +} + +/** + * Utility method to skip middle update calls, optimistically if update function is called multiple times with few different parameters, only the last variant will be executed. + * The last invocation is executed after a few cycles, allowing to skip middle ones. + * + * This method can be used inside Svelte components to collapse complex update logic and handle interactions. + */ +export function reduceCalls) => Promise> ( + operation: T +): (...args: ReduceParameters) => Promise { + let nextCall: NextCall | undefined + let currentCall: NextCall | undefined + + const next = (): void => { + currentCall = nextCall + nextCall = undefined + if (currentCall !== undefined) { + void currentCall.op().catch() + } + } + return async function (...args: ReduceParameters): Promise { + const myOp = async (): Promise => { + try { + await operation(...args) + } catch (err: any) { + console.error('Error occurred in reduceCalls:', err) + } + next() + } + + nextCall = { op: myOp } + await Promise.resolve() + if (currentCall === undefined) { + next() + } + } +} + +export function isOwnerOrMaintainer (): boolean { + const account = getCurrentAccount() + return hasAccountRole(account, AccountRole.Maintainer) +} + +export function hasAccountRole (acc: Account, targerRole: AccountRole): boolean { + return roleOrder[acc.role] >= roleOrder[targerRole] +} + +export function getBranding (brandings: BrandingMap, key: string | undefined): Branding | null { + if (key === undefined) return null + + return Object.values(brandings).find((branding) => branding.key === key) ?? null +} + +export function fillConfiguration (systemTx: Tx[], configs: Map, PluginConfiguration>): void { + for (const t of systemTx) { + if (t._class === core.class.TxCreateDoc) { + const ct = t as TxCreateDoc + if (ct.objectClass === core.class.PluginConfiguration) { + configs.set(ct.objectId as Ref, TxProcessor.createDoc2Doc(ct) as PluginConfiguration) + } + } else if (t._class === core.class.TxUpdateDoc) { + const ut = t as TxUpdateDoc + if (ut.objectClass === core.class.PluginConfiguration) { + const c = configs.get(ut.objectId as Ref) + if (c !== undefined) { + if (c.system !== true || ut.modifiedBy === core.account.ConfigUser) { + TxProcessor.updateDoc2Doc(c, ut) + } + } + } + } + } +} + +export function pluginFilterTx ( + excludedPlugins: PluginConfiguration[], + configs: Map, PluginConfiguration>, + systemTx: Tx[] +): Tx[] { + const stx = toIdMap(systemTx) + const totalExcluded = new Set>() + let msg = '' + for (const a of excludedPlugins) { + for (const c of configs.values()) { + if (a.pluginId === c.pluginId) { + for (const id of c.transactions) { + if (c.classFilter !== undefined) { + const filter = new Set(c.classFilter) + const tx = stx.get(id as Ref) + if ( + tx?._class === core.class.TxCreateDoc || + tx?._class === core.class.TxUpdateDoc || + tx?._class === core.class.TxRemoveDoc + ) { + const cud = tx as TxCUD + if (filter.has(cud.objectClass)) { + totalExcluded.add(id as Ref) + } + } + } else { + totalExcluded.add(id as Ref) + } + } + msg += ` ${c.pluginId}:${c.transactions.length}` + } + } + } + if (typeof window !== 'undefined') { + console.log('exclude plugin', msg) + } + systemTx = systemTx.filter((t) => !totalExcluded.has(t._id)) + return systemTx +} + +/** + * @public + */ +export class TimeRateLimiter { + idCounter: number = 0 + active: number = 0 + last: number = 0 + rate: number + period: number + executions: { time: number, running: boolean }[] = [] + + queue: (() => Promise)[] = [] + notify: (() => void)[] = [] + + constructor (rate: number, period: number = 1000) { + this.rate = rate + this.period = period + } + + private cleanupExecutions (): void { + const now = Date.now() + this.executions = this.executions.filter((time) => time.running || now - time.time < this.period) + } + + async exec = any>(op: (args?: B) => Promise, args?: B): Promise { + while (this.active >= this.rate || this.executions.length >= this.rate) { + this.cleanupExecutions() + if (this.executions.length < this.rate) { + break + } + await new Promise((resolve) => { + setTimeout(resolve, this.period / this.rate) + }) + } + + const v = { time: Date.now(), running: true } + this.active++ + try { + this.executions.push(v) + const p = op(args) + return await p + } finally { + v.running = false + this.active-- + this.cleanupExecutions() + const n = this.notify.shift() + if (n !== undefined) { + n() + } + } + } + + async waitProcessing (): Promise { + while (this.active > 0) { + await new Promise((resolve) => { + this.notify.push(resolve) + }) + } + } +} + +export function combineAttributes ( + attributes: any[], + key: string, + operator: '$push' | '$pull' | '$unset', + arrayKey?: '$each' | '$in' +): any[] { + return Array.from( + new Set( + attributes.flatMap((attr) => { + if (arrayKey === undefined) { + return attr[operator]?.[key] + } + + return Array.isArray(attr[operator]?.[key]?.[arrayKey]) + ? attr[operator]?.[key]?.[arrayKey] + : attr[operator]?.[key] + }) + ) + ).filter((v) => v != null) +} + +export function buildSocialIdString (key: SocialKey): string { + return `${key.type}:${key.value}` +} + +export function parseSocialIdString (id: string): SocialKey { + const [type, value] = id.split(':') + + if (type === undefined || value === undefined) { + throw new Error(`Social id is not valid: ${id}`) + } + + if (!Object.values(SocialIdType).includes(type as SocialIdType)) { + throw new Error(`Social id type is not valid: ${id}`) + } + + return { type: type as SocialIdType, value } +} + +export function pickPrimarySocialId (socialIds: SocialId[]): SocialId { + const activeSocialIds = socialIds.filter((si) => si.isDeleted !== true) + if (activeSocialIds.length === 0) { + throw new Error('No active social ids provided') + } + const hulySocialIds = activeSocialIds.filter((si) => si.type === SocialIdType.HULY) + + return hulySocialIds[0] ?? activeSocialIds[0] +} + +export const loginSocialTypes = [SocialIdType.EMAIL, SocialIdType.GOOGLE, SocialIdType.GITHUB, SocialIdType.OIDC] + +export function notEmpty (id: T | undefined | null): id is T { + return id !== undefined && id !== null && id !== '' +} + +export function unique (arr: T[]): T[] { + return Array.from(new Set(arr)) +} + +export function uniqueNotEmpty> (arr: Array): T[] { + return unique(arr).filter(notEmpty) +} +export { platformNow, platformNowDiff } from '@hcengineering/measurements' + +export interface PermissionsGrant { + spaces?: Ref[] + grantedBy?: AccountUuid +} + +export type KeysByType = { [k in keyof O]-?: O[k] extends T ? k : never }[keyof O] + +export function toRank (str: string | undefined): Rank | undefined { + if (str === undefined) return + if (str.startsWith('0|')) { + return str + } + return '0|' + str.replaceAll(/[-:_]/g, '').toLowerCase() +} diff --git a/foundations/core/packages/core/tsconfig.json b/foundations/core/packages/core/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/core/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/hulylake-client/.eslintrc.js b/foundations/core/packages/hulylake-client/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/hulylake-client/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/hulylake-client/.npmignore b/foundations/core/packages/hulylake-client/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/hulylake-client/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/hulylake-client/CHANGELOG.json b/foundations/core/packages/hulylake-client/CHANGELOG.json new file mode 100644 index 0000000000..e84136358d --- /dev/null +++ b/foundations/core/packages/hulylake-client/CHANGELOG.json @@ -0,0 +1,86 @@ +{ + "name": "@hcengineering/hulylake-client", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/hulylake-client_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.6", + "tag": "@hcengineering/hulylake-client_v0.7.6", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + }, + { + "comment": "Updating dependency \"@hcengineering/retry\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/hulylake-client_v0.7.5", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/retry\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/hulylake-client_v0.7.4", + "date": "Thu, 09 Oct 2025 16:57:55 GMT", + "comments": { + "patch": [ + { + "comment": "refactoring hulylake client to extract multi-workspace client" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/hulylake-client_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/hulylake-client/CHANGELOG.md b/foundations/core/packages/hulylake-client/CHANGELOG.md new file mode 100644 index 0000000000..8e4c0f294c --- /dev/null +++ b/foundations/core/packages/hulylake-client/CHANGELOG.md @@ -0,0 +1,35 @@ +# Change Log - @hcengineering/hulylake-client + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.6 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.5 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.4 +Thu, 09 Oct 2025 16:57:55 GMT + +### Patches + +- refactoring hulylake client to extract multi-workspace client + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/hulylake-client/config/rig.json b/foundations/core/packages/hulylake-client/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/hulylake-client/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/hulylake-client/jest.config.js b/foundations/core/packages/hulylake-client/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/hulylake-client/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/hulylake-client/package.json b/foundations/core/packages/hulylake-client/package.json new file mode 100644 index 0000000000..1cea5891eb --- /dev/null +++ b/foundations/core/packages/hulylake-client/package.json @@ -0,0 +1,60 @@ +{ + "name": "@hcengineering/hulylake-client", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent --coverage", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@types/jest": "^29.5.5", + "@types/node": "^22.18.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "cross-env": "~7.0.3", + "esbuild": "^0.25.10", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "prettier": "^3.6.2", + "ts-jest": "^29.1.1", + "typescript": "^5.9.3", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/retry": "workspace:^0.7.17" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/hulylake-client/src/client.ts b/foundations/core/packages/hulylake-client/src/client.ts new file mode 100644 index 0000000000..af88ca5199 --- /dev/null +++ b/foundations/core/packages/hulylake-client/src/client.ts @@ -0,0 +1,349 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { WorkspaceUuid } from '@hcengineering/core' +import { RetryOptions } from '@hcengineering/retry' + +import { fetchSafe, unwrapContentLength, unwrapEtag, unwrapLastModified } from './utils' +import { + HulyHeaders, + HulylakeClient, + HulylakeWorkspaceClient, + HulyMeta, + HulyResponse, + JsonPatch, + PatchOptions, + PutOptions, + Body +} from './types' + +export function getWorkspaceClient (baseUrl: string, workspace: WorkspaceUuid, token: string): HulylakeWorkspaceClient { + const client = new Client(baseUrl, token) + return new WorkspaceClient(client, workspace) +} + +export function getClient (baseUrl: string, token: string): HulylakeClient { + return new Client(baseUrl, token) +} + +class WorkspaceClient implements HulylakeWorkspaceClient { + constructor ( + private readonly client: HulylakeClient, + private readonly workspace: WorkspaceUuid + ) {} + + head (key: string, retryOptions?: RetryOptions): Promise> { + return this.client.head(this.workspace, key, retryOptions) + } + + get (key: string, retryOptions?: RetryOptions): Promise>> { + return this.client.get(this.workspace, key, retryOptions) + } + + put (key: string, body: Body, opts: PutOptions, retryOptions?: RetryOptions): Promise> { + return this.client.put(this.workspace, key, body, opts, retryOptions) + } + + patch (key: string, body: Body, opts: PatchOptions, retryOptions?: RetryOptions): Promise> { + return this.client.patch(this.workspace, key, body, opts, retryOptions) + } + + delete (key: string, retryOptions?: RetryOptions): Promise> { + return this.client.delete(this.workspace, key, retryOptions) + } + + public async getJson(key: string, retryOptions?: RetryOptions): Promise> { + const res = await this.client.get(this.workspace, key, retryOptions) + const body = res.ok && res.body != null ? ((await new Response(res.body).json()) as T) : undefined + return { ...res, body } + } + + public async putJson( + key: string, + json: T, + options?: Omit, + retryOptions?: RetryOptions + ): Promise> { + return await this.put(key, JSON.stringify(json), { ...options, mergeStrategy: 'jsonpatch' }, retryOptions) + } + + public async patchJson ( + key: string, + body: JsonPatch[], + options?: Omit, + retryOptions?: RetryOptions + ): Promise> { + return await this.patch( + key, + JSON.stringify(body), + { ...options, contentType: 'application/json-patch+json' }, + retryOptions + ) + } +} + +class Client implements HulylakeClient { + constructor ( + private readonly baseUrl: string, + private readonly token: string + ) { + this.baseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl + } + + private authHeaders (init?: HeadersInit): Headers { + const headers = new Headers(init) + headers.set('Authorization', `Bearer ${this.token}`) + return headers + } + + private applyHeaders (h: Headers, headers?: HulyHeaders): void { + if (headers != null) for (const [k, v] of Object.entries(headers)) h.set(`huly-header-${k}`, v) + } + + private applyMeta (h: Headers, meta?: HulyMeta): void { + if (meta != null) for (const [k, v] of Object.entries(meta)) h.set(`huly-meta-${k}`, v) + } + + public async status (): Promise { + try { + const res = await fetchSafe(`${this.baseUrl}/status`) + return res.ok + } catch { + return false + } + } + + public objectUrl (workspace: string, key: string): string { + return `${this.baseUrl}/api/${workspace}/${encodeURIComponent(key)}` + } + + public async head (workspace: string, key: string, retryOptions?: RetryOptions): Promise> { + const res = await fetchSafe( + this.objectUrl(workspace, key), + { + method: 'HEAD', + headers: this.authHeaders() + }, + retryOptions + ) + + return { + ok: res.ok, + status: res.status, + etag: unwrapEtag(res.headers.get('ETag')), + lastModified: unwrapLastModified(res.headers.get('Last-Modified')), + contentType: res.headers.get('Content-Type') ?? 'application/octet-stream', + contentLength: unwrapContentLength(res.headers.get('Content-Length')), + headers: res.headers + } + } + + public async get ( + workspace: string, + key: string, + retryOptions?: RetryOptions + ): Promise>> { + try { + const res = await fetchSafe( + this.objectUrl(workspace, key), + { + method: 'GET', + headers: this.authHeaders() + }, + retryOptions + ) + + return { + ok: res.ok, + status: res.status, + etag: unwrapEtag(res.headers.get('ETag')), + headers: res.headers, + lastModified: unwrapLastModified(res.headers.get('Last-Modified')), + contentType: res.headers.get('Content-Type') ?? 'application/octet-stream', + contentLength: unwrapContentLength(res.headers.get('Content-Length')), + body: res.ok ? (res.body ?? undefined) : undefined + } + } catch (err: any) { + if (err.name === 'NotFoundError') { + return { + ok: false, + status: 404, + etag: undefined, + headers: new Headers(), + body: undefined + } + } + throw err + } + } + + public async partial ( + workspace: string, + key: string, + offset: number, + length?: number, + retryOptions?: RetryOptions + ): Promise>> { + try { + const res = await fetchSafe( + this.objectUrl(workspace, key), + { + method: 'GET', + headers: this.authHeaders({ + Range: length !== undefined ? `bytes=${offset}-${offset + length - 1}` : `bytes=${offset}` + }) + }, + retryOptions + ) + + return { + ok: res.ok, + status: res.status, + etag: unwrapEtag(res.headers.get('ETag')), + headers: res.headers, + body: res.ok ? (res.body ?? undefined) : undefined + } + } catch (err: any) { + if (err.name === 'NotFoundError') { + return { + ok: false, + status: 404, + etag: undefined, + headers: new Headers(), + body: undefined + } + } + throw err + } + } + + public async put ( + workspace: string, + key: string, + body: Body, + opts: PutOptions = {}, + retryOptions?: RetryOptions + ): Promise> { + const { mergeStrategy, headers, meta } = opts + const contentType = 'contentType' in opts ? opts.contentType : undefined + const contentLength = 'contentLength' in opts ? opts.contentLength : undefined + + const h = this.authHeaders() + + if (mergeStrategy != null) { + h.set('Huly-Merge-Strategy', mergeStrategy) + } + + if (contentType != null) { + h.set('Content-Type', contentType) + } else if (mergeStrategy === 'jsonpatch') { + h.set('Content-Type', 'application/json') + } + + if (contentLength != null) { + h.set('Content-Length', contentLength.toString()) + } + + this.applyHeaders(h, headers) + this.applyMeta(h, meta) + + const duplex = body instanceof ReadableStream ? 'half' : undefined + + const res = await fetchSafe( + this.objectUrl(workspace, key), + { + method: 'PUT', + headers: h, + body, + // @ts-expect-error must present for ReadableStream but it is not in the interface + duplex + }, + retryOptions + ) + + return { + ok: res.ok, + status: res.status, + etag: unwrapEtag(res.headers.get('ETag')), + lastModified: unwrapLastModified(res.headers.get('Last-Modified')), + contentLength: unwrapContentLength(res.headers.get('Content-Length')), + headers: res.headers + } + } + + public async patch ( + workspace: string, + key: string, + body: Body, + opts: PatchOptions = {}, + retryOptions?: RetryOptions + ): Promise> { + const { contentType, contentLength, headers, meta } = opts + + const h = this.authHeaders() + + if (contentType != null) { + h.set('Content-Type', contentType) + } + + if (contentLength != null) { + h.set('Content-Length', contentLength.toString()) + } + + this.applyHeaders(h, headers) + this.applyMeta(h, meta) + + const duplex = body instanceof ReadableStream ? 'half' : undefined + + const res = await fetchSafe( + this.objectUrl(workspace, key), + { + method: 'PATCH', + headers: h, + body, + // @ts-expect-error must present for ReadableStream but it is not in the interface + duplex + }, + retryOptions + ) + + return { + ok: res.ok, + status: res.status, + etag: unwrapEtag(res.headers.get('ETag')), + lastModified: unwrapLastModified(res.headers.get('Last-Modified')), + contentLength: unwrapContentLength(res.headers.get('Content-Length')), + contentType: res.headers.get('Content-Type') ?? undefined, + headers: res.headers + } + } + + public async delete (workspace: string, key: string, retryOptions?: RetryOptions): Promise> { + const res = await fetchSafe( + this.objectUrl(workspace, key), + { + method: 'DELETE', + headers: this.authHeaders() + }, + retryOptions + ) + + return { + ok: res.ok, + status: res.status, + headers: res.headers + } + } +} diff --git a/foundations/core/packages/hulylake-client/src/error.ts b/foundations/core/packages/hulylake-client/src/error.ts new file mode 100644 index 0000000000..a26cbd711a --- /dev/null +++ b/foundations/core/packages/hulylake-client/src/error.ts @@ -0,0 +1,28 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export class NetworkError extends Error { + constructor (message: string) { + super(message) + this.name = 'NetworkError' + } +} + +export class HulylakeError extends Error { + constructor (message: string) { + super(message) + this.name = 'HulylakeError' + } +} diff --git a/foundations/core/packages/hulylake-client/src/index.ts b/foundations/core/packages/hulylake-client/src/index.ts new file mode 100644 index 0000000000..0f078ded71 --- /dev/null +++ b/foundations/core/packages/hulylake-client/src/index.ts @@ -0,0 +1,17 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './client' +export * from './types' diff --git a/foundations/core/packages/hulylake-client/src/types.ts b/foundations/core/packages/hulylake-client/src/types.ts new file mode 100644 index 0000000000..c9c53eb0d6 --- /dev/null +++ b/foundations/core/packages/hulylake-client/src/types.ts @@ -0,0 +1,120 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { RetryOptions } from '@hcengineering/retry' + +export interface HulylakeClient { + head: (workspace: string, key: string, retryOptions?: RetryOptions) => Promise> + get: ( + workspace: string, + key: string, + retryOptions?: RetryOptions + ) => Promise>> + partial: ( + workspace: string, + key: string, + offset: number, + length?: number, + retryOptions?: RetryOptions + ) => Promise>> + put: ( + workspace: string, + key: string, + body: Body, + opts: PutOptions, + retryOptions?: RetryOptions + ) => Promise> + patch: ( + workspace: string, + key: string, + body: Body, + opts: PatchOptions, + retryOptions?: RetryOptions + ) => Promise> + delete: (workspace: string, key: string, retryOptions?: RetryOptions) => Promise> + + objectUrl: (workspace: string, key: string) => string +} + +export interface HulylakeWorkspaceClient { + head: (key: string, retryOptions?: RetryOptions) => Promise> + get: (key: string, retryOptions?: RetryOptions) => Promise>> + put: (key: string, body: Body, opts: PutOptions, retryOptions?: RetryOptions) => Promise> + patch: (key: string, body: Body, opts: PatchOptions, retryOptions?: RetryOptions) => Promise> + delete: (key: string, retryOptions?: RetryOptions) => Promise> + + getJson: (key: string, retryOptions?: RetryOptions) => Promise> + putJson: ( + key: string, + json: T, + options?: Omit, + retryOptions?: RetryOptions + ) => Promise> + patchJson: ( + key: string, + body: JsonPatch[], + options?: Omit, + retryOptions?: RetryOptions + ) => Promise> +} + +export type Body = ReadableStream | ArrayBuffer | Blob | string +export type MergeStrategy = 'concatenate' | 'jsonpatch' +export type HulyHeaders = Record +export type HulyMeta = Record + +export type PutOptions = + | { + mergeStrategy?: 'concatenate' + contentLength?: number + contentType?: string + headers?: HulyHeaders + meta?: HulyMeta + } + | { + mergeStrategy: 'jsonpatch' + contentLength?: number + headers?: HulyHeaders + meta?: HulyMeta + } + +export interface PatchOptions { + contentLength?: number + contentType?: string + headers?: HulyHeaders + meta?: HulyMeta +} + +export type JsonPatch = + | { op: 'add', path: string, value: any } + | { op: 'replace', path: string, value: any } + | { op: 'remove', path: string } + | { op: 'move', from: string, path: string } + | { op: 'copy', from: string, path: string } + | { op: 'test', path: string, value: any } + | { hop: 'add', path: string, value: any, safe?: boolean } + | { hop: 'inc', path: string, value: number, safe?: boolean } + | { hop: 'remove', path: string, safe?: boolean } + +export interface HulyResponse { + ok: boolean + status: number + etag?: string + contentType?: string + contentLength?: number + lastModified?: number + headers: Headers + body?: Body +} diff --git a/foundations/core/packages/hulylake-client/src/utils.ts b/foundations/core/packages/hulylake-client/src/utils.ts new file mode 100644 index 0000000000..1d93750e86 --- /dev/null +++ b/foundations/core/packages/hulylake-client/src/utils.ts @@ -0,0 +1,75 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { RetryOptions, withRetry } from '@hcengineering/retry' +import { HulylakeError, NetworkError } from './error' + +async function innerFetchSafe (url: string | URL, init?: RequestInit): Promise { + let response + try { + response = await fetch(url, init) + } catch (err: any) { + console.error('network error', { err }) + throw new NetworkError(`Network error ${err}`) + } + + if (response.ok) { + return response + } + + if (response.status === 404) { + return response + } + + const text = await response.text() + throw new HulylakeError(text) +} + +export async function fetchSafe (url: string | URL, init?: RequestInit, retryOptions?: RetryOptions): Promise { + if (retryOptions != null) { + return await withRetry(async () => await innerFetchSafe(url, init), retryOptions) + } + return await innerFetchSafe(url, init) +} + +export function unwrapEtag (etag: string | null | undefined): string | undefined { + if (etag == null) { + return undefined + } + + if (etag.startsWith('W/')) { + etag = etag.substring(2) + } + + if (etag.startsWith('"') && etag.endsWith('"')) { + etag = etag.slice(1, -1) + } + + return etag +} + +export function unwrapContentLength (length: string | null | undefined): number | undefined { + if (length == null) { + return undefined + } + return parseInt(length, 10) +} + +export function unwrapLastModified (lastModified: string | null | undefined): number | undefined { + if (lastModified == null) { + return undefined + } + return Date.parse(lastModified) +} diff --git a/foundations/core/packages/hulylake-client/tsconfig.json b/foundations/core/packages/hulylake-client/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/hulylake-client/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/measurements-otlp/.eslintrc.js b/foundations/core/packages/measurements-otlp/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/measurements-otlp/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/measurements-otlp/.npmignore b/foundations/core/packages/measurements-otlp/.npmignore new file mode 100644 index 0000000000..9b083cb0c4 --- /dev/null +++ b/foundations/core/packages/measurements-otlp/.npmignore @@ -0,0 +1,8 @@ +* +!/lib/** +!/types/** +!/src/** +!CHANGELOG.md +/lib/**/__test__/ +/types/**/__test__/ +/src/**/__test__/ \ No newline at end of file diff --git a/foundations/core/packages/measurements-otlp/CHANGELOG.json b/foundations/core/packages/measurements-otlp/CHANGELOG.json new file mode 100644 index 0000000000..97439bfeb5 --- /dev/null +++ b/foundations/core/packages/measurements-otlp/CHANGELOG.json @@ -0,0 +1,91 @@ +{ + "name": "@hcengineering/measurements-otlp", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/measurements-otlp_v0.7.17", + "date": "Mon, 27 Oct 2025 15:24:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/measurements\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.13", + "tag": "@hcengineering/measurements-otlp_v0.7.13", + "date": "Mon, 27 Oct 2025 04:08:57 GMT", + "comments": { + "patch": [ + { + "comment": "Allow to suspend errors on change" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/measurements\" from `^0.7.13` to `0.7.14`" + } + ] + } + }, + { + "version": "0.7.12", + "tag": "@hcengineering/measurements-otlp_v0.7.12", + "date": "Sat, 11 Oct 2025 19:18:56 GMT", + "comments": { + "patch": [ + { + "comment": "rollback eslint" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/measurements\" from `^0.7.11` to `0.7.12`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.14` to `0.7.15`" + } + ] + } + }, + { + "version": "0.7.11", + "tag": "@hcengineering/measurements-otlp_v0.7.11", + "date": "Sat, 11 Oct 2025 17:58:53 GMT", + "comments": { + "none": [ + { + "comment": "Add tests" + } + ], + "patch": [ + { + "comment": "Fix eslint deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/measurements\" from `^0.7.10` to `0.7.11`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.12` to `0.7.13`" + } + ] + } + }, + { + "version": "0.7.10", + "tag": "@hcengineering/measurements-otlp_v0.7.10", + "date": "Fri, 10 Oct 2025 12:32:59 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.10` to `0.7.11`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/measurements-otlp/CHANGELOG.md b/foundations/core/packages/measurements-otlp/CHANGELOG.md new file mode 100644 index 0000000000..95464fcff8 --- /dev/null +++ b/foundations/core/packages/measurements-otlp/CHANGELOG.md @@ -0,0 +1,35 @@ +# Change Log - @hcengineering/measurements-otlp + +This log was last generated on Mon, 27 Oct 2025 15:24:19 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 15:24:19 GMT + +_Version update only_ + +## 0.7.13 +Mon, 27 Oct 2025 04:08:57 GMT + +### Patches + +- Allow to suspend errors on change + +## 0.7.12 +Sat, 11 Oct 2025 19:18:56 GMT + +### Patches + +- rollback eslint + +## 0.7.11 +Sat, 11 Oct 2025 17:58:53 GMT + +### Patches + +- Fix eslint deps + +## 0.7.10 +Fri, 10 Oct 2025 12:32:59 GMT + +_Initial release_ + diff --git a/foundations/core/packages/measurements-otlp/config/rig.json b/foundations/core/packages/measurements-otlp/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/measurements-otlp/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/measurements-otlp/jest.config.js b/foundations/core/packages/measurements-otlp/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/foundations/core/packages/measurements-otlp/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/foundations/core/packages/measurements-otlp/package.json b/foundations/core/packages/measurements-otlp/package.json new file mode 100644 index 0000000000..6c030b5f6c --- /dev/null +++ b/foundations/core/packages/measurements-otlp/package.json @@ -0,0 +1,71 @@ +{ + "name": "@hcengineering/measurements-otlp", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "!lib/**/__test__/**", + "types/**/*", + "!types/**/__test__/**", + "src/**/*", + "!src/**/__test__/**", + "README.md", + "CHANGELOG.md" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0" + }, + "dependencies": { + "@hcengineering/measurements": "workspace:^0.7.18", + "@opentelemetry/sdk-node": "^0.203.0", + "@opentelemetry/sdk-logs": "^0.203.0", + "@opentelemetry/auto-instrumentations-node": "^0.62.0", + "@opentelemetry/resources": "^2.0.1", + "@opentelemetry/sdk-trace-node": "^2.0.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.1", + "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.203.0", + "@opentelemetry/otlp-exporter-base": "^0.203.0", + "@opentelemetry/id-generator-aws-xray": "^2.0.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.203.0", + "@opentelemetry/api-logs": "^0.203.0", + "@opentelemetry/sdk-metrics": "^2.0.1" + }, + "repository": "https://github.com/hcengineering/huly.utils", + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + }, + "publishConfig": { + "access": "public" + } +} diff --git a/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.suspendError.test.ts b/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.suspendError.test.ts new file mode 100644 index 0000000000..3955ca0a89 --- /dev/null +++ b/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.suspendError.test.ts @@ -0,0 +1,51 @@ +import { OpenTelemetryMetricsContext } from '../telemetry' + +describe('OpenTelemetryMetricsContext suspendErrors handling', () => { + test('when suspendErrors is true the span does NOT record exception or set status', async () => { + const mockSpan = { + recordException: jest.fn(), + setStatus: jest.fn(), + end: jest.fn(), + setAttribute: jest.fn() + } as any + + const mockTracer = { + startSpan: jest.fn().mockReturnValue(mockSpan) + } as any + + const ctx = new OpenTelemetryMetricsContext('root', mockTracer, undefined, undefined, {}) + + await expect( + ctx.with( + 'op', + {}, + () => Promise.reject(new Error('boom')), // operation returns a rejected promise + undefined, + { suspendErrors: true } + ) + ).rejects.toThrow('boom') + + expect(mockSpan.recordException).not.toHaveBeenCalled() + expect(mockSpan.setStatus).not.toHaveBeenCalled() + }) + + test('when suspendErrors is not true the span records exception and sets error status', async () => { + const mockSpan = { + recordException: jest.fn(), + setStatus: jest.fn(), + end: jest.fn(), + setAttribute: jest.fn() + } as any + + const mockTracer = { + startSpan: jest.fn().mockReturnValue(mockSpan) + } as any + + const ctx = new OpenTelemetryMetricsContext('root', mockTracer, undefined, undefined, {}) + + await expect(ctx.with('op', {}, () => Promise.reject(new Error('boom')), undefined, {})).rejects.toThrow('boom') + + expect(mockSpan.recordException).toHaveBeenCalled() + expect(mockSpan.setStatus).toHaveBeenCalledWith(expect.objectContaining({ message: 'boom' })) + }) +}) diff --git a/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.test.ts b/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.test.ts new file mode 100644 index 0000000000..660147942f --- /dev/null +++ b/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.test.ts @@ -0,0 +1,451 @@ +import { newMetrics, type MeasureLogger } from '@hcengineering/measurements' +import { context, trace } from '@opentelemetry/api' +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node' +import { OpenTelemetryMetricsContext } from '../telemetry' + +describe('telemetry', () => { + let tracer: any + let provider: NodeTracerProvider + + beforeAll(() => { + provider = new NodeTracerProvider() + provider.register() + tracer = trace.getTracer('test-tracer') + }) + + afterAll(() => { + void provider.shutdown() + }) + + describe('OpenTelemetryMetricsContext', () => { + const mockLogger: MeasureLogger = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + close: jest.fn(async () => {}), + logOperation: jest.fn() + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should create a new context with tracer', () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'test', + tracer, + undefined, + undefined, + { op: 'create' }, + {}, + metrics, + mockLogger + ) + + expect(ctx).toBeDefined() + expect(ctx.metrics).toBe(metrics) + expect(ctx.logger).toBe(mockLogger) + }) + + it('should create child context with span', () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'parent', + tracer, + context.active(), + undefined, + {}, + {}, + metrics, + mockLogger + ) + + const child = ctx.newChild('child', { op: 'child' }, { span: true }) + + expect(child).toBeDefined() + expect(child.parent).toBe(ctx) + }) + + it('should create child context without span when span is false', () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'parent', + tracer, + context.active(), + undefined, + {}, + {}, + metrics, + mockLogger + ) + + const child = ctx.newChild('child', { op: 'child' }, { span: false }) + + expect(child).toBeDefined() + }) + + it('should execute async operation with context', async () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'test', + tracer, + context.active(), + undefined, + {}, + {}, + metrics, + mockLogger + ) + + let executed = false + await ctx.with('operation', { op: 'test' }, async () => { + executed = true + await new Promise((resolve) => setTimeout(resolve, 10)) + }) + + expect(executed).toBe(true) + expect(metrics.measurements.operation).toBeDefined() + }) + + it('should execute sync operation with context', () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'test', + tracer, + context.active(), + undefined, + {}, + {}, + metrics, + mockLogger + ) + + const result = ctx.withSync('operation', { op: 'test' }, () => { + return 42 + }) + + expect(result).toBe(42) + expect(metrics.measurements.operation).toBeDefined() + }) + + it('should handle errors in async operations', async () => { + const metrics = newMetrics() + const span = tracer.startSpan('test-span') + const ctx = new OpenTelemetryMetricsContext('test', tracer, context.active(), span, {}, {}, metrics, mockLogger) + + await expect( + ctx.with('operation', { op: 'test' }, async () => { + throw new Error('Test error') + }) + ).rejects.toThrow('Test error') + + expect(metrics.measurements.operation).toBeDefined() + }) + + it('should measure custom value with meter', () => { + const metrics = newMetrics() + const mockMeter = { + getCounter: jest.fn(() => ({ + counter: { record: jest.fn() }, + value: 0 + })) + } + + const ctx = new OpenTelemetryMetricsContext( + 'test', + tracer, + undefined, + undefined, + { op: 'test' }, + {}, + metrics, + mockLogger, + undefined, + undefined, + undefined, + mockMeter as any + ) + + ctx.measure('custom', 100) + + expect(mockMeter.getCounter).toHaveBeenCalledWith('custom') + }) + + it('should log info with OTLP logger', () => { + const metrics = newMetrics() + const mockOtlpLogger = { + emit: jest.fn() + } + + const ctx = new OpenTelemetryMetricsContext( + 'test', + tracer, + context.active(), + undefined, + { op: 'test' }, + {}, + metrics, + mockLogger, + undefined, + undefined, + mockOtlpLogger as any + ) + + ctx.info('Test message', { key: 'value' }) + + expect(mockOtlpLogger.emit).toHaveBeenCalled() + expect(mockLogger.info).toHaveBeenCalled() + }) + + it('should log error with OTLP logger', () => { + const metrics = newMetrics() + const mockOtlpLogger = { + emit: jest.fn() + } + + const ctx = new OpenTelemetryMetricsContext( + 'test', + tracer, + context.active(), + undefined, + { op: 'test' }, + {}, + metrics, + mockLogger, + undefined, + undefined, + mockOtlpLogger as any + ) + + ctx.error('Error message', { error: 'details' }) + + expect(mockOtlpLogger.emit).toHaveBeenCalled() + expect(mockLogger.error).toHaveBeenCalled() + }) + + it('should log warn with OTLP logger', () => { + const metrics = newMetrics() + const mockOtlpLogger = { + emit: jest.fn() + } + + const ctx = new OpenTelemetryMetricsContext( + 'test', + tracer, + context.active(), + undefined, + { op: 'test' }, + {}, + metrics, + mockLogger, + undefined, + undefined, + mockOtlpLogger as any + ) + + ctx.warn('Warning message', { warning: 'info' }) + + expect(mockOtlpLogger.emit).toHaveBeenCalled() + expect(mockLogger.warn).toHaveBeenCalled() + }) + + it('should extract metadata from context', () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'test', + tracer, + context.active(), + undefined, + {}, + {}, + metrics, + mockLogger + ) + + const meta = ctx.extractMeta() + + expect(meta).toBeDefined() + expect(typeof meta).toBe('object') + }) + + it('should get params', () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'test', + tracer, + undefined, + undefined, + { op: 'test', method: 'GET' }, + {}, + metrics, + mockLogger + ) + + const params = ctx.getParams() + + expect(params).toEqual({ op: 'test', method: 'GET' }) + }) + + it('should share contextData with children', () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'parent', + tracer, + context.active(), + undefined, + {}, + {}, + metrics, + mockLogger + ) + ctx.contextData = { userId: '123' } + + const child = ctx.newChild('child', {}) + + expect(child.contextData).toBe(ctx.contextData) + }) + + it('should inherit params when inheritParams is true', async () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'parent', + tracer, + context.active(), + undefined, + { parentKey: 'parentValue' }, + {}, + metrics, + mockLogger + ) + + await ctx.with( + 'child', + { childKey: 'childValue' }, + async (childCtx) => { + expect(childCtx.getParams()).toEqual({ childKey: 'childValue' }) + }, + {}, + { inheritParams: false } + ) + + await ctx.with( + 'child', + { childKey: 'childValue' }, + async (childCtx) => { + expect(childCtx.getParams()).toEqual({ + parentKey: 'parentValue', + childKey: 'childValue' + }) + }, + {}, + { inheritParams: true } + ) + }) + + it('should not call end() twice', () => { + const metrics = newMetrics() + const span = tracer.startSpan('test-span') + const spanEndSpy = jest.spyOn(span, 'end') + + const ctx = new OpenTelemetryMetricsContext('test', tracer, context.active(), span, {}, {}, metrics, mockLogger) + + ctx.end() + ctx.end() // Second call should be no-op + + expect(spanEndSpy).toHaveBeenCalledTimes(1) + }) + + it('should handle null return value', async () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'test', + tracer, + context.active(), + undefined, + {}, + {}, + metrics, + mockLogger + ) + + const result = await ctx.with('operation', {}, () => null) + + expect(result).toBeUndefined() + }) + + it('should log operation when log option is true', async () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'test', + tracer, + context.active(), + undefined, + {}, + {}, + metrics, + mockLogger + ) + + await ctx.with( + 'operation', + { op: 'test' }, + async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + }, + {}, + { log: true } + ) + + expect(mockLogger.logOperation).toHaveBeenCalledWith( + 'operation', + expect.any(Number), + expect.objectContaining({ op: 'test' }) + ) + }) + + it('should suppress tracing when span is disable', () => { + const metrics = newMetrics() + const ctx = new OpenTelemetryMetricsContext( + 'parent', + tracer, + context.active(), + undefined, + {}, + {}, + metrics, + mockLogger + ) + + const child = ctx.newChild('child', {}, { span: 'disable' }) + + expect(child).toBeDefined() + }) + + it('should set span attributes from params', () => { + const metrics = newMetrics() + + const ctx = new OpenTelemetryMetricsContext( + 'parent', + tracer, + context.active(), + undefined, + {}, + {}, + metrics, + mockLogger + ) + + // When creating a child with span: true, attributes are set on the child's span + const child = ctx.newChild( + 'child', + { key1: 'value1', key2: 'value2' }, + { span: true } + ) as OpenTelemetryMetricsContext + + // The child should have been created successfully + expect(child).toBeDefined() + expect(child.parent).toBe(ctx) + }) + }) +}) diff --git a/foundations/core/packages/measurements-otlp/src/index.ts b/foundations/core/packages/measurements-otlp/src/index.ts new file mode 100644 index 0000000000..8b5ba81c39 --- /dev/null +++ b/foundations/core/packages/measurements-otlp/src/index.ts @@ -0,0 +1 @@ +export * from './telemetry' diff --git a/foundations/core/packages/measurements-otlp/src/telemetry.ts b/foundations/core/packages/measurements-otlp/src/telemetry.ts new file mode 100644 index 0000000000..c1770a01a3 --- /dev/null +++ b/foundations/core/packages/measurements-otlp/src/telemetry.ts @@ -0,0 +1,632 @@ +import { + childMetrics, + consoleLogger, + MeasureContext, + MeasureMetricsContext, + newMetrics, + noParamsLogger, + nullPromise, + platformNow, + platformNowDiff, + updateMeasure, + type FullParamsType, + type MeasureLogger, + type Metrics, + type ParamsType, + type WithOptions +} from '@hcengineering/measurements' +import { + context, + metrics as otelMetrics, + propagation, + Span, + SpanStatusCode, + trace, + type Context, + type Gauge, + type Meter, + type Tracer +} from '@opentelemetry/api' +import { Logger, SeverityNumber } from '@opentelemetry/api-logs' +import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node' +import { suppressTracing } from '@opentelemetry/core' +import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http' +import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http' +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' +import { AWSXRayIdGenerator } from '@opentelemetry/id-generator-aws-xray' +import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base' +import { resourceFromAttributes } from '@opentelemetry/resources' +import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs' +import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics' +import { NodeSDK } from '@opentelemetry/sdk-node' +import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node' + +class MetricsContext { + counters = new Map() + constructor (readonly meter?: Meter) {} + + getCounter (name: string): { counter: Gauge, value: number } | undefined { + if (this.meter === undefined) { + return undefined + } + let counter = this.counters.get(name) + if (counter === undefined) { + counter = { counter: this.meter.createGauge(name), value: 0 } + this.counters.set(name, counter) + } + return counter + } +} + +/** + * @public + */ +export class OpenTelemetryMetricsContext implements MeasureContext { + private readonly name: string + private readonly params: ParamsType + + private readonly fullParams: FullParamsType | (() => FullParamsType) = {} + logger: MeasureLogger + metrics: Metrics + id?: string + + st = platformNow() + contextData: object = {} + isDone = false + doneTrace: string = '' + + private done (value?: number, override?: boolean): void { + if (!this.isDone) { + this.doneTrace = new Error().stack ?? '' + this.isDone = true + updateMeasure(this.metrics, this.st, this.params, this.fullParams, (spend) => {}, value, override) + this.span?.end() + } + } + + constructor ( + name: string, + readonly tracer: Tracer, + readonly context: Context | undefined, + readonly span: Span | undefined, + params: ParamsType, + fullParams: FullParamsType | (() => FullParamsType) = {}, + metrics: Metrics = newMetrics(), + logger?: MeasureLogger, + readonly parent?: MeasureContext, + readonly logParams?: ParamsType, + + readonly otlpLogger?: Logger, + readonly meter?: MetricsContext + ) { + this.name = name + this.params = params + this.fullParams = fullParams + this.metrics = metrics + this.metrics.namedParams = this.metrics.namedParams ?? {} + for (const [k, v] of Object.entries(params)) { + if (this.metrics.namedParams[k] !== v) { + this.metrics.namedParams[k] = v + } else { + this.metrics.namedParams[k] = '*' + } + } + + this.logger = logger ?? (this.logParams != null ? consoleLogger(this.logParams ?? {}) : noParamsLogger) + } + + measure (name: string, value: number, override?: boolean): void { + const cnt = this.meter?.getCounter(name) + if (cnt !== undefined) { + if (cnt.value !== value) { + cnt.counter.record(value, this.params) + cnt.value = value + } + } + } + + newChild ( + name: string, + params: ParamsType, + opt?: { + fullParams?: FullParamsType + logger?: MeasureLogger + span?: WithOptions['span'] // By default true + meta?: Record + } + ): MeasureContext { + let _span: Span | undefined + let childContext: Context | undefined + if (opt?.span === true || opt?.span === 'inherit') { + childContext = opt?.span === 'inherit' ? context.active() : (this.context ?? context.active()) + + if (opt.meta !== undefined && Object.keys(opt.meta).length > 0) { + // We need to set meta params + childContext = propagation.extract(childContext ?? context.active(), opt.meta) + } + _span = this.tracer.startSpan(name, undefined, childContext) + + const spanParams = [...Object.entries(params)] + for (const [k, v] of spanParams) { + _span?.setAttribute(k, v as any) + } + } + if (opt?.span === 'disable') { + childContext = suppressTracing(childContext ?? context.active()) + } + if (childContext !== undefined && _span !== undefined) { + childContext = trace.setSpan(childContext, _span) + } + + const result = new OpenTelemetryMetricsContext( + name, + this.tracer, + childContext, + _span, + params, + opt?.fullParams ?? {}, + childMetrics(this.metrics, [name]), + opt?.logger ?? this.logger, + this, + this.logParams, + this.otlpLogger, + this.meter + ) + result.id = this.id + result.contextData = this.contextData + return result + } + + extractMeta (): Record { + const headers: Record = {} + if (this.context !== undefined) { + propagation.inject(this.context, headers) + } + return headers + } + + with( + name: string, + params: ParamsType, + op: (ctx: MeasureContext) => T | Promise, + fullParams?: ParamsType | (() => FullParamsType), + opt?: WithOptions + ): Promise { + const c = this.newChild(name, opt?.inheritParams === true ? { ...this.params, ...params } : params, { + fullParams, + logger: this.logger, + span: opt?.span ?? true, + meta: opt?.meta + }) + let needFinally = true + try { + const _context = (c as OpenTelemetryMetricsContext).context + + const span = (c as OpenTelemetryMetricsContext).span + + const value = _context !== undefined ? context.with(_context, () => op(c)) : op(c) + if (value instanceof Promise) { + needFinally = false + if (span !== undefined && opt?.suspendErrors !== true) { + void value.catch((err) => { + span?.recordException(err) + span?.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message + }) + }) + } + return value.finally(() => { + if (span !== undefined) { + const fParams = typeof fullParams === 'function' ? fullParams() : fullParams + const spanParams = [...Object.entries(params), ...Object.entries(fParams ?? {})] + for (const [k, v] of spanParams) { + span?.setAttribute(k, typeof v === 'object' ? JSON.stringify(v) : v) + } + } + c.end() + if (opt?.log === true) { + this.logger.logOperation(name, platformNowDiff((c as OpenTelemetryMetricsContext).st), { + ...params, + ...fullParams + }) + } + }) + } else { + if (value == null) { + return nullPromise as Promise + } + return Promise.resolve(value) + } + } finally { + if (needFinally) { + c.end() + } + } + } + + withSync( + name: string, + params: ParamsType, + op: (ctx: MeasureContext) => T, + fullParams?: ParamsType | (() => FullParamsType), + opt?: WithOptions + ): T { + const c = this.newChild(name, params, { fullParams, logger: this.logger, span: opt?.span ?? true }) + const _context = (c as OpenTelemetryMetricsContext).context + try { + return _context !== undefined ? context.with(_context, () => op(c)) : op(c) + } finally { + c.end() + } + } + + error (message: string, args?: Record): void { + if (this.otlpLogger !== undefined) { + this.otlpLogger.emit({ + severityNumber: SeverityNumber.ERROR, + severityText: 'error', + context: this.context, + body: message, + attributes: { + 'service.name': sdkServiceName, + ...(args ?? {}) + } + }) + } + this.logger.error(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) + } + + info (message: string, args?: Record): void { + if (this.otlpLogger !== undefined) { + this.otlpLogger.emit({ + context: this.context, + severityNumber: SeverityNumber.INFO, + severityText: 'info', + body: message, + attributes: { + 'service.name': sdkServiceName, + ...(args ?? {}) + } + }) + } + this.logger.info(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) + } + + warn (message: string, args?: Record): void { + if (this.otlpLogger !== undefined) { + this.otlpLogger.emit({ + severityNumber: SeverityNumber.WARN, + severityText: 'warn', + context: this.context, + body: message, + attributes: { + 'service.name': sdkServiceName, + ...(args ?? {}) + } + }) + } + this.logger.warn(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) + } + + end (): void { + this.done() + } + + getParams (): ParamsType { + return this.params + } +} + +/** + * Parse W3C baggage header format to Record + * + * W3C baggage format: "key1=value1,key2=value2;property=value,key3=value3" + * Returns only the key-value pairs, ignoring properties + */ +function parseBaggage (baggageHeader?: string): Record { + if (baggageHeader == null || typeof baggageHeader !== 'string') { + return {} + } + + const result: Record = {} + + // Split by comma to get individual baggage members + const members = baggageHeader.split(',') + + for (const member of members) { + const trimmedMember = member.trim() + if (trimmedMember === '') continue + + // Split by semicolon to separate key=value from properties + const parts = trimmedMember.split(';') + const keyValuePart = parts[0]?.trim() + + if (keyValuePart == null) continue + + // Split by equals to get key and value + const equalIndex = keyValuePart.indexOf('=') + if (equalIndex === -1) continue + + const key = keyValuePart.substring(0, equalIndex).trim() + const value = keyValuePart.substring(equalIndex + 1).trim() + + if (key != null) { + // URL decode the key and value + try { + const decodedKey = decodeURIComponent(key) + const decodedValue = decodeURIComponent(value) + result[decodedKey] = decodedValue + } catch (error) { + // If decoding fails, use the original values + result[key] = value + } + } + } + + return result +} + +let sdk: NodeSDK | undefined +let sdkServiceName: string | undefined +let sdkServiceVersion: string | undefined +let loggerProvider: LoggerProvider | undefined + +export function initOpenTelemetrySDK (serviceName: string, version: string): boolean { + if (sdk !== undefined) { + return true + } + process.env.OTEL_SERVICE_NAME = serviceName + process.env.OTEL_SERVICE_VERSION = version + + sdkServiceName = serviceName + sdkServiceVersion = version + const tracesUrl = getTracesUrl() + + if (tracesUrl === undefined) { + return false + } + + const traceHeaders = parseTraceExporterHeaders() + + const exporter = new OTLPTraceExporter({ + url: tracesUrl, + headers: traceHeaders, + compression: + (process.env.OTEL_EXPORTER_OTLP_COMPRESSION as CompressionAlgorithm) ?? + (process.env.OTEL_EXPORTER_OTLP_TRACES_COMPRESSION as CompressionAlgorithm) ?? + CompressionAlgorithm.GZIP, + keepAlive: true + }) + + const batchSpanProcessor = new BatchSpanProcessor(exporter, { + maxExportBatchSize: parseInt(process.env.OTEL_EXPORTER_OTLP_TRACES_MAX_EXPORT_BATCH_SIZE ?? '1000'), + maxQueueSize: parseInt(process.env.OTEL_EXPORTER_OTLP_TRACES_MAX_QUEUE_SIZE ?? '1000') + }) + + // Logs + const logsEndpoint = getLogsUrl() + const logHeaders = parseLogsExporterHeaders() + const logExporter = new OTLPLogExporter({ + url: logsEndpoint, + headers: logHeaders, + compression: + (process.env.OTEL_EXPORTER_OTLP_COMPRESSION as CompressionAlgorithm) ?? + (process.env.OTEL_EXPORTER_OTLP_LOGS_COMPRESSION as CompressionAlgorithm) ?? + CompressionAlgorithm.GZIP, + keepAlive: true + }) + + const batchLogProcessor = new BatchLogRecordProcessor(logExporter, { + maxExportBatchSize: parseInt(process.env.OTEL_EXPORTER_OTLP_LOGS_MAX_EXPORT_BATCH_SIZE ?? '1000'), + maxQueueSize: parseInt(process.env.OTEL_EXPORTER_OTLP_LOGS_MAX_QUEUE_SIZE ?? '1000') + }) + + // Metrics + + const metricsUrl = getMetricsUrl() + const metricsHeaders = parseMetricsExporterHeaders() + const metricsExporter = new OTLPMetricExporter({ + url: metricsUrl, + headers: metricsHeaders + }) + const metricReader = new PeriodicExportingMetricReader({ + exporter: metricsExporter, + exportIntervalMillis: 15000 + }) + + // SDK + + sdk = new NodeSDK({ + spanProcessors: [batchSpanProcessor], + serviceName, + traceExporter: exporter, + resource: resourceFromAttributes({ + 'service-name': serviceName, + 'service-version': version ?? '0.7', + 'deployment-environment': process.env.OTEL_ENVIRONMENT + }), + instrumentations: [getNodeAutoInstrumentations()], + idGenerator: new AWSXRayIdGenerator(), + logRecordProcessors: [batchLogProcessor], + metricReader + }) + + sdk.start() + + loggerProvider = new LoggerProvider({ + processors: [batchLogProcessor] + }) + + // Graceful shutdown + process.on('SIGTERM', () => { + sdk + ?.shutdown() + .then(() => { + console.log('Tracing terminated') + }) + .catch((error) => { + console.log('Error terminating tracing', error) + }) + .finally(() => process.exit(0)) + }) + console.log('Using open telemetry metrics context', { + traceEndpoint: tracesUrl, + tracerHeadersSet: Array.from(Object.keys(traceHeaders)), + logsEndpoint, + logHeadersSet: Array.from(Object.keys(logHeaders)) + }) + return true +} + +export function reportOTELError (error: Error, attributes?: Record): void { + if (sdkServiceName !== undefined && sdkServiceVersion !== undefined && loggerProvider !== undefined) { + const otlpLogger = loggerProvider?.getLogger(sdkServiceName, sdkServiceVersion) + otlpLogger?.emit({ + severityNumber: SeverityNumber.ERROR, + severityText: 'error', + body: error.message, + context: context.active(), + attributes: { + ...attributes, + 'service.name': sdkServiceName, + 'service.version': sdkServiceVersion, + 'error.stack': error.stack + } + }) + } +} + +export function reportOTEL ( + severity: 'info' | 'warning', + message: string, + time: number, + attributes?: Record +): void { + if (sdkServiceName !== undefined && sdkServiceVersion !== undefined && loggerProvider !== undefined) { + const otlpLogger = loggerProvider?.getLogger(sdkServiceName, sdkServiceVersion) + otlpLogger?.emit({ + severityNumber: severity === 'info' ? SeverityNumber.INFO : SeverityNumber.WARN, + body: message, + severityText: severity, + timestamp: time, + context: context.active(), + attributes: { + ...attributes, + 'service.name': sdkServiceName, + 'service.version': sdkServiceVersion + } + }) + } +} + +export function createOpenTelemetryMetricsContext ( + name: string, + params: ParamsType, + fullParams: FullParamsType | (() => FullParamsType) = {}, + metrics: Metrics = newMetrics(), + logger?: MeasureLogger, + version?: string +): MeasureContext { + if (!initOpenTelemetrySDK(name, version ?? '')) { + console.warn('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is not set, OpenTelemetry metrics will not be sent') + return new MeasureMetricsContext(name, params, fullParams, metrics, logger) + } + + // Traces + + const tracer = trace.getTracer(name) + + const otlpLogger = + process.env.OTEL_LOGGER_ENABLED === 'true' ? loggerProvider?.getLogger(sdkServiceName ?? name, version) : undefined + + const meter = otelMetrics.getMeter(name, version) + + const ctx = new OpenTelemetryMetricsContext( + name, + tracer, + undefined, + undefined, + params, + fullParams, + metrics, + logger, + undefined, + undefined, + otlpLogger, + new MetricsContext(meter) + ) + return ctx +} +function parseTraceExporterHeaders (): Record { + const headers: Record = parseBaggage(process.env.OTEL_EXPORTER_OTLP_HEADERS) ?? {} + + if (process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS !== undefined) { + const extraHeaders = parseBaggage(process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS) + for (const [key, value] of Object.entries(extraHeaders)) { + headers[key] = value + } + } + return headers +} + +function getTracesUrl (): string | undefined { + let tracesUrl = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT + + if (tracesUrl !== undefined && !tracesUrl.endsWith('/v1/traces')) { + if (tracesUrl.endsWith('/')) { + tracesUrl += 'v1/traces' + } else { + tracesUrl += '/v1/traces' + } + } + return tracesUrl +} + +function getLogsUrl (): string | undefined { + let logsUrl = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT + + if (logsUrl !== undefined && !logsUrl.endsWith('/v1/logs')) { + if (logsUrl.endsWith('/')) { + logsUrl += 'v1/logs' + } else { + logsUrl += '/v1/logs' + } + } + return logsUrl +} +function parseLogsExporterHeaders (): Record { + const headers: Record = parseBaggage(process.env.OTEL_EXPORTER_OTLP_HEADERS) ?? {} + + if (process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS !== undefined) { + const extraHeaders = parseBaggage(process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS) + for (const [key, value] of Object.entries(extraHeaders)) { + headers[key] = value + } + } + return headers +} + +function getMetricsUrl (): string | undefined { + let metricsUrl = process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT + + if (metricsUrl !== undefined && !metricsUrl.endsWith('/v1/metrics')) { + if (metricsUrl.endsWith('/')) { + metricsUrl += 'v1/metrics' + } else { + metricsUrl += '/v1/metrics' + } + } + return metricsUrl +} +function parseMetricsExporterHeaders (): Record { + const headers: Record = parseBaggage(process.env.OTEL_EXPORTER_OTLP_HEADERS) ?? {} + + if (process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS !== undefined) { + const extraHeaders = parseBaggage(process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS) + for (const [key, value] of Object.entries(extraHeaders)) { + headers[key] = value + } + } + return headers +} diff --git a/foundations/core/packages/measurements-otlp/tsconfig.json b/foundations/core/packages/measurements-otlp/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/measurements-otlp/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/measurements/.eslintrc.js b/foundations/core/packages/measurements/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/measurements/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/measurements/.npmignore b/foundations/core/packages/measurements/.npmignore new file mode 100644 index 0000000000..9b083cb0c4 --- /dev/null +++ b/foundations/core/packages/measurements/.npmignore @@ -0,0 +1,8 @@ +* +!/lib/** +!/types/** +!/src/** +!CHANGELOG.md +/lib/**/__test__/ +/types/**/__test__/ +/src/**/__test__/ \ No newline at end of file diff --git a/foundations/core/packages/measurements/CHANGELOG.json b/foundations/core/packages/measurements/CHANGELOG.json new file mode 100644 index 0000000000..58fb5458b0 --- /dev/null +++ b/foundations/core/packages/measurements/CHANGELOG.json @@ -0,0 +1,97 @@ +{ + "name": "@hcengineering/measurements", + "entries": [ + { + "version": "0.7.18", + "tag": "@hcengineering/measurements_v0.7.18", + "date": "Mon, 27 Oct 2025 15:24:19 GMT", + "comments": { + "patch": [ + { + "comment": "Fix withContext to use options" + } + ] + } + }, + { + "version": "0.7.14", + "tag": "@hcengineering/measurements_v0.7.14", + "date": "Mon, 27 Oct 2025 04:08:57 GMT", + "comments": { + "patch": [ + { + "comment": "Allow to suspend errors on change" + } + ] + } + }, + { + "version": "0.7.13", + "tag": "@hcengineering/measurements_v0.7.13", + "date": "Tue, 14 Oct 2025 03:02:36 GMT", + "comments": { + "patch": [ + { + "comment": "Remove zero value measure with override values" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.18` to `0.7.19`" + } + ] + } + }, + { + "version": "0.7.12", + "tag": "@hcengineering/measurements_v0.7.12", + "date": "Sat, 11 Oct 2025 19:18:56 GMT", + "comments": { + "patch": [ + { + "comment": "rollback eslint" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.14` to `0.7.15`" + } + ] + } + }, + { + "version": "0.7.11", + "tag": "@hcengineering/measurements_v0.7.11", + "date": "Sat, 11 Oct 2025 17:58:53 GMT", + "comments": { + "none": [ + { + "comment": "Add tests" + } + ], + "patch": [ + { + "comment": "Fix eslint deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.12` to `0.7.13`" + } + ] + } + }, + { + "version": "0.7.10", + "tag": "@hcengineering/measurements_v0.7.10", + "date": "Fri, 10 Oct 2025 12:32:59 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.10` to `0.7.11`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/measurements/CHANGELOG.md b/foundations/core/packages/measurements/CHANGELOG.md new file mode 100644 index 0000000000..70a571fc57 --- /dev/null +++ b/foundations/core/packages/measurements/CHANGELOG.md @@ -0,0 +1,44 @@ +# Change Log - @hcengineering/measurements + +This log was last generated on Mon, 27 Oct 2025 15:24:19 GMT and should not be manually modified. + +## 0.7.18 +Mon, 27 Oct 2025 15:24:19 GMT + +### Patches + +- Fix withContext to use options + +## 0.7.14 +Mon, 27 Oct 2025 04:08:57 GMT + +### Patches + +- Allow to suspend errors on change + +## 0.7.13 +Tue, 14 Oct 2025 03:02:36 GMT + +### Patches + +- Remove zero value measure with override values + +## 0.7.12 +Sat, 11 Oct 2025 19:18:56 GMT + +### Patches + +- rollback eslint + +## 0.7.11 +Sat, 11 Oct 2025 17:58:53 GMT + +### Patches + +- Fix eslint deps + +## 0.7.10 +Fri, 10 Oct 2025 12:32:59 GMT + +_Initial release_ + diff --git a/foundations/core/packages/measurements/config/rig.json b/foundations/core/packages/measurements/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/measurements/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/measurements/jest.config.js b/foundations/core/packages/measurements/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/foundations/core/packages/measurements/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/foundations/core/packages/measurements/package.json b/foundations/core/packages/measurements/package.json new file mode 100644 index 0000000000..bdcdd1768c --- /dev/null +++ b/foundations/core/packages/measurements/package.json @@ -0,0 +1,54 @@ +{ + "name": "@hcengineering/measurements", + "version": "0.7.18", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "!lib/**/__test__/**", + "types/**/*", + "!types/**/__test__/**", + "src/**/*", + "!src/**/__test__/**", + "README.md", + "CHANGELOG.md" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0" + }, + "repository": "https://github.com/hcengineering/platform", + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + }, + "publishConfig": { + "access": "public" + } +} diff --git a/foundations/core/packages/measurements/src/__tests__/context.test.ts b/foundations/core/packages/measurements/src/__tests__/context.test.ts new file mode 100644 index 0000000000..d59d0a4945 --- /dev/null +++ b/foundations/core/packages/measurements/src/__tests__/context.test.ts @@ -0,0 +1,467 @@ +import { + MeasureMetricsContext, + NoMetricsContext, + consoleLogger, + noParamsLogger, + withContext, + setOperationLogProfiling, + registerOperationLog, + updateOperationLog, + addOperation +} from '../context' +import { newMetrics } from '../metrics' +import type { MeasureLogger, MeasureContext } from '../types' + +describe('context', () => { + describe('consoleLogger', () => { + it('should create a logger with params', () => { + const logger = consoleLogger({ service: 'test' }) + + expect(logger).toBeDefined() + expect(typeof logger.info).toBe('function') + expect(typeof logger.error).toBe('function') + expect(typeof logger.warn).toBe('function') + expect(typeof logger.close).toBe('function') + }) + + it('should log info messages', () => { + const consoleSpy = jest.spyOn(console, 'info').mockImplementation() + const logger = consoleLogger({ service: 'test' }) + + logger.info('Test message', { key: 'value' }) + + expect(consoleSpy).toHaveBeenCalled() + consoleSpy.mockRestore() + }) + + it('should log error messages', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation() + const logger = consoleLogger({ service: 'test' }) + + logger.error('Error message', { error: 'details' }) + + expect(consoleSpy).toHaveBeenCalled() + consoleSpy.mockRestore() + }) + + it('should log warn messages', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation() + const logger = consoleLogger({ service: 'test' }) + + logger.warn('Warning message', { warning: 'info' }) + + expect(consoleSpy).toHaveBeenCalled() + consoleSpy.mockRestore() + }) + + it('should handle errors in params', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation() + const logger = consoleLogger({}) + const error = new Error('Test error') + + logger.error('Error occurred', { error }) + + expect(consoleSpy).toHaveBeenCalled() + const call = consoleSpy.mock.calls[0] + expect(call[0]).toBe('Error occurred') + consoleSpy.mockRestore() + }) + }) + + describe('MeasureMetricsContext', () => { + let logger: MeasureLogger + + beforeEach(() => { + logger = noParamsLogger + }) + + it('should create a new context', () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', { op: 'create' }, {}, metrics, logger) + + expect(ctx).toBeDefined() + expect(ctx.metrics).toBe(metrics) + expect(ctx.logger).toBe(logger) + }) + + it('should measure operation duration', async () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', { op: 'create' }, {}, metrics, logger) + + await new Promise((resolve) => setTimeout(resolve, 50)) + ctx.end() + + expect(metrics.operations).toBe(1) + expect(metrics.value).toBeGreaterThan(40) + }) + + it('should create child context', () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('parent', { op: 'parent' }, {}, metrics, logger) + const child = ctx.newChild('child', { op: 'child' }) + + expect(child).toBeDefined() + expect(child.parent).toBe(ctx) + }) + + it('should execute async operation with context', async () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, logger) + + let executed = false + await ctx.with('operation', { op: 'test' }, async () => { + executed = true + await new Promise((resolve) => setTimeout(resolve, 10)) + }) + + expect(executed).toBe(true) + expect(metrics.measurements.operation).toBeDefined() + }) + + it('should execute sync operation with context', () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, logger) + + const result = ctx.withSync('operation', { op: 'test' }, () => { + return 42 + }) + + expect(result).toBe(42) + expect(metrics.measurements.operation).toBeDefined() + }) + + it('should measure custom value', () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, logger) + + ctx.measure('custom', 100) + + expect(metrics.measurements['#custom']).toBeDefined() + expect(metrics.measurements['#custom'].value).toBe(100) + }) + + it('should log info messages', () => { + const mockLogger: MeasureLogger = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + close: jest.fn(async () => {}), + logOperation: jest.fn() + } + + const ctx = new MeasureMetricsContext('test', { op: 'test' }, {}, newMetrics(), mockLogger) + ctx.info('Test message', { key: 'value' }) + + expect(mockLogger.info).toHaveBeenCalledWith( + 'Test message', + expect.objectContaining({ key: 'value', op: 'test' }) + ) + }) + + it('should log error messages', () => { + const mockLogger: MeasureLogger = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + close: jest.fn(async () => {}), + logOperation: jest.fn() + } + + const ctx = new MeasureMetricsContext('test', { op: 'test' }, {}, newMetrics(), mockLogger) + ctx.error('Error message', { error: 'details' }) + + expect(mockLogger.error).toHaveBeenCalledWith( + 'Error message', + expect.objectContaining({ error: 'details', op: 'test' }) + ) + }) + + it('should log warn messages', () => { + const mockLogger: MeasureLogger = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + close: jest.fn(async () => {}), + logOperation: jest.fn() + } + + const ctx = new MeasureMetricsContext('test', { op: 'test' }, {}, newMetrics(), mockLogger) + ctx.warn('Warning message', { warning: 'info' }) + + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Warning message', + expect.objectContaining({ warning: 'info', op: 'test' }) + ) + }) + + it('should get params', () => { + const ctx = new MeasureMetricsContext('test', { op: 'test', method: 'GET' }, {}, newMetrics(), logger) + const params = ctx.getParams() + + expect(params).toEqual({ op: 'test', method: 'GET' }) + }) + + it('should share contextData with children', () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('parent', {}, {}, metrics, logger) + ctx.contextData = { userId: '123' } + + const child = ctx.newChild('child', {}) + + expect(child.contextData).toBe(ctx.contextData) + }) + + it('should handle named parameters', () => { + const metrics = newMetrics() + const r = new MeasureMetricsContext('test', { method: 'GET' }, {}, metrics, logger) + expect(r).toBeDefined() + expect(metrics.namedParams.method).toBe('GET') + }) + + it('should update named parameters on multiple contexts', () => { + const metrics = newMetrics() + const ctx1 = new MeasureMetricsContext('test1', { method: 'GET' }, {}, metrics, logger) + expect(ctx1).toBeDefined() + expect(metrics.namedParams.method).toBe('GET') + + // Create another context with different value for same param + const ctx2 = new MeasureMetricsContext('test2', { method: 'POST' }, {}, metrics, logger) + expect(ctx2).toBeDefined() + + // The second context will see existing value is different, so it will update to '*' + // But this happens within the constructor logic + expect(metrics.namedParams.method).toBe('POST') + }) + }) + + describe('NoMetricsContext', () => { + it('should create a no-op context', () => { + const ctx = new NoMetricsContext() + + expect(ctx).toBeDefined() + expect(ctx.logger).toBeDefined() + }) + + it('should execute operations without measuring', async () => { + const ctx = new NoMetricsContext() + + let executed = false + await ctx.with('operation', {}, async () => { + executed = true + }) + + expect(executed).toBe(true) + }) + + it('should create child contexts', () => { + const ctx = new NoMetricsContext() + const child = ctx.newChild('child', {}) + + expect(child).toBeDefined() + expect(child).toBeInstanceOf(NoMetricsContext) + }) + + it('should handle measure calls without error', () => { + const ctx = new NoMetricsContext() + + expect(() => { + ctx.measure('test', 100) + }).not.toThrow() + }) + + it('should handle end calls without error', () => { + const ctx = new NoMetricsContext() + + expect(() => { + ctx.end() + }).not.toThrow() + }) + + it('should return empty params', () => { + const ctx = new NoMetricsContext() + const params = ctx.getParams() + + expect(params).toEqual({}) + }) + }) + + describe('withContext decorator', () => { + it('should wrap method with context', async () => { + class TestClass { + @withContext('testOperation', { service: 'test' }) + async testMethod (ctx: MeasureContext, value: number): Promise { + return value * 2 + } + } + + const instance = new TestClass() + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger) + + const result = await instance.testMethod(ctx, 21) + + expect(result).toBe(42) + expect(metrics.measurements.testOperation).toBeDefined() + }) + }) + + describe('operation log profiling', () => { + beforeEach(() => { + setOperationLogProfiling(false) + }) + + afterEach(() => { + setOperationLogProfiling(false) + }) + + it('should register operation log when profiling enabled', () => { + setOperationLogProfiling(true) + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) + + const { opLogMetrics, op } = registerOperationLog(ctx) + + expect(opLogMetrics).toBe(metrics) + expect(op).toBeDefined() + expect(ctx.id).toBeDefined() + }) + + it('should not register operation log when profiling disabled', () => { + setOperationLogProfiling(false) + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) + + const { opLogMetrics, op } = registerOperationLog(ctx) + + expect(opLogMetrics).toBeUndefined() + expect(op).toBeUndefined() + }) + + it('should update operation log', () => { + setOperationLogProfiling(true) + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) + + const { opLogMetrics, op } = registerOperationLog(ctx) + updateOperationLog(opLogMetrics, op) + + expect(op?.end).toBeGreaterThan(0) + }) + + it('should add operation to log', async () => { + setOperationLogProfiling(true) + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) + + registerOperationLog(ctx) + + let executed = false + await addOperation(ctx, 'asyncOp', { op: 'test' }, async () => { + executed = true + }) + + expect(executed).toBe(true) + expect(metrics.opLog).toBeDefined() + }) + + it('should limit operation log entries', () => { + setOperationLogProfiling(true) + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) + + registerOperationLog(ctx) + + // Create many operation log entries + if (metrics.opLog != null && ctx.id != null) { + for (let i = 0; i < 50; i++) { + metrics.opLog[ctx.id].ops.push({ + op: `test${i}`, + start: i, + end: i + 1, + params: {} + }) + } + + const op = metrics.opLog[ctx.id] + updateOperationLog(metrics, op) + + expect(Object.keys(metrics.opLog).length).toBeLessThanOrEqual(31) + } + }) + }) + + describe('extractMeta', () => { + it('should return empty metadata', () => { + const ctx = new MeasureMetricsContext('test', {}, {}, newMetrics(), noParamsLogger) + const meta = ctx.extractMeta() + + expect(meta).toEqual({}) + }) + }) + + describe('async operations', () => { + it('should handle promise rejection', async () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) + + await expect( + ctx.with('operation', { op: 'test' }, async () => { + throw new Error('Test error') + }) + ).rejects.toThrow('Test error') + + expect(metrics.measurements.operation).toBeDefined() + }) + + it('should return null promise for null sync result', async () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) + + const result = await ctx.with('operation', { op: 'test' }, () => { + return null + }) + + expect(result).toBeUndefined() + }) + + it('should handle sync return value', async () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) + + const result = await ctx.with('operation', { op: 'test' }, () => { + return 42 + }) + + expect(result).toBe(42) + }) + + it('should log operation when log option is true', async () => { + const mockLogger: MeasureLogger = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + close: jest.fn(async () => {}), + logOperation: jest.fn() + } + + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('test', {}, {}, metrics, mockLogger) + + await ctx.with( + 'operation', + { op: 'test' }, + async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + }, + {}, + { log: true } + ) + + expect(mockLogger.logOperation).toHaveBeenCalledWith( + 'operation', + expect.any(Number), + expect.objectContaining({ op: 'test' }) + ) + }) + }) +}) diff --git a/foundations/core/packages/measurements/src/__tests__/index.test.ts b/foundations/core/packages/measurements/src/__tests__/index.test.ts new file mode 100644 index 0000000000..d701edd4e2 --- /dev/null +++ b/foundations/core/packages/measurements/src/__tests__/index.test.ts @@ -0,0 +1,47 @@ +import { platformNow, platformNowDiff } from '../index' + +describe('index', () => { + describe('platformNow', () => { + it('should return a positive number', () => { + const now = platformNow() + expect(typeof now).toBe('number') + expect(now).toBeGreaterThan(0) + }) + + it('should return increasing values over time', async () => { + const first = platformNow() + await new Promise((resolve) => setTimeout(resolve, 10)) + const second = platformNow() + expect(second).toBeGreaterThan(first) + }) + }) + + describe('platformNowDiff', () => { + it('should calculate the difference between timestamps', async () => { + const start = platformNow() + await new Promise((resolve) => setTimeout(resolve, 50)) + const diff = platformNowDiff(start) + + expect(diff).toBeGreaterThan(40) + expect(diff).toBeLessThan(100) + }) + + it('should round to 2 decimal places', async () => { + const start = platformNow() + await new Promise((resolve) => setTimeout(resolve, 10)) + const diff = platformNowDiff(start) + + // Check that it has at most 2 decimal places + const decimalPlaces = (diff.toString().split('.')[1] ?? '').length + expect(decimalPlaces).toBeLessThanOrEqual(2) + }) + + it('should handle very small differences', () => { + const start = platformNow() + const diff = platformNowDiff(start) + + expect(diff).toBeGreaterThanOrEqual(0) + expect(typeof diff).toBe('number') + }) + }) +}) diff --git a/foundations/core/packages/measurements/src/__tests__/metrics.test.ts b/foundations/core/packages/measurements/src/__tests__/metrics.test.ts new file mode 100644 index 0000000000..e10136fa72 --- /dev/null +++ b/foundations/core/packages/measurements/src/__tests__/metrics.test.ts @@ -0,0 +1,297 @@ +import { + newMetrics, + measure, + childMetrics, + metricsAggregate, + metricsToString, + metricsToJson, + metricsToRows, + updateMeasure +} from '../metrics' +import { platformNow } from '../index' + +describe('metrics', () => { + describe('newMetrics', () => { + it('should create a new metrics object with default values', () => { + const metrics = newMetrics() + + expect(metrics.operations).toBe(0) + expect(metrics.value).toBe(0) + expect(metrics.measurements).toEqual({}) + expect(metrics.params).toEqual({}) + expect(metrics.namedParams).toEqual({}) + }) + }) + + describe('measure', () => { + it('should measure operation duration', async () => { + const metrics = newMetrics() + const done = measure(metrics, { operation: 'test' }) + + await new Promise((resolve) => setTimeout(resolve, 50)) + done() + + expect(metrics.operations).toBe(1) + expect(metrics.value).toBeGreaterThan(40) + }) + + it('should call endOp callback with duration', async () => { + const metrics = newMetrics() + let capturedSpend = 0 + const done = measure(metrics, { operation: 'test' }, {}, (spend) => { + capturedSpend = spend + }) + + await new Promise((resolve) => setTimeout(resolve, 50)) + done() + + expect(capturedSpend).toBeGreaterThan(40) + }) + + it('should handle multiple operations', async () => { + const metrics = newMetrics() + + const done1 = measure(metrics, { operation: 'test' }) + await new Promise((resolve) => setTimeout(resolve, 20)) + done1() + + const done2 = measure(metrics, { operation: 'test' }) + await new Promise((resolve) => setTimeout(resolve, 20)) + done2() + + expect(metrics.operations).toBe(2) + expect(metrics.value).toBeGreaterThan(30) + }) + }) + + describe('updateMeasure', () => { + it('should update metrics with custom value', () => { + const metrics = newMetrics() + const st = platformNow() + + updateMeasure(metrics, st, { op: 'test' }, {}, undefined, 100) + + expect(metrics.operations).toBe(1) + expect(metrics.value).toBe(100) + }) + + it('should override operations when override is true', () => { + const metrics = newMetrics() + const st = platformNow() + + // First call without override - accumulates value and increments operations + updateMeasure(metrics, st, { op: 'test' }, {}, undefined, 50, false) + expect(metrics.value).toBe(50) + expect(metrics.operations).toBe(1) + + // Second call with override - sets operations, doesn't add to value + updateMeasure(metrics, st, { op: 'test' }, {}, undefined, 100, true) + + expect(metrics.operations).toBe(100) // overridden + expect(metrics.value).toBe(50) // not changed when override=true + }) + + it('should track parameters', () => { + const metrics = newMetrics() + const st = platformNow() + + updateMeasure(metrics, st, { method: 'GET' }, {}, undefined, 100) + updateMeasure(metrics, st, { method: 'POST' }, {}, undefined, 200) + + expect(metrics.params.method).toBeDefined() + expect(metrics.params.method.GET).toBeDefined() + expect(metrics.params.method.POST).toBeDefined() + expect(metrics.params.method.GET.value).toBe(100) + expect(metrics.params.method.POST.value).toBe(200) + }) + + it('should handle multiple parameters as counters', () => { + const metrics = newMetrics() + const st = platformNow() + + updateMeasure(metrics, st, { method: 'GET', status: '200' }, {}, undefined, 100) + updateMeasure(metrics, st, { method: 'GET', status: '404' }, {}, undefined, 50) + + expect(metrics.params.method.GET).toBeDefined() + expect(metrics.params.method.GET.topResult).toBeDefined() + expect(metrics.params.method.GET.topResult?.length).toBeGreaterThan(0) + }) + + it('should update top results', () => { + const metrics = newMetrics() + const st = platformNow() + + updateMeasure(metrics, st, {}, { request: 'slow' }, undefined, 100) + updateMeasure(metrics, st, {}, { request: 'fast' }, undefined, 10) + + expect(metrics.topResult).toBeDefined() + expect(metrics.topResult?.length).toBeGreaterThan(0) + }) + }) + + describe('childMetrics', () => { + it('should create child metrics in hierarchy', () => { + const root = newMetrics() + const child = childMetrics(root, ['level1', 'level2']) + + expect(root.measurements.level1).toBeDefined() + expect(root.measurements.level1.measurements.level2).toBeDefined() + expect(child).toBe(root.measurements.level1.measurements.level2) + }) + + it('should reuse existing child metrics', () => { + const root = newMetrics() + const child1 = childMetrics(root, ['level1']) + const child2 = childMetrics(root, ['level1']) + + expect(child1).toBe(child2) + }) + + it('should create nested paths', () => { + const root = newMetrics() + childMetrics(root, ['api', 'users', 'create']) + + expect(root.measurements.api).toBeDefined() + expect(root.measurements.api.measurements.users).toBeDefined() + expect(root.measurements.api.measurements.users.measurements.create).toBeDefined() + }) + }) + + describe('metricsAggregate', () => { + it('should aggregate metrics', () => { + const metrics = newMetrics() + metrics.value = 100 + metrics.operations = 10 + + const child1 = childMetrics(metrics, ['child1']) + child1.value = 50 + child1.operations = 5 + + const child2 = childMetrics(metrics, ['child2']) + child2.value = 30 + child2.operations = 3 + + const aggregated = metricsAggregate(metrics) + + expect(aggregated.value).toBe(80) // child1 + child2 + expect(aggregated.operations).toBe(10) + }) + + it('should limit number of child metrics', () => { + const metrics = newMetrics() + + for (let i = 0; i < 10; i++) { + const child = childMetrics(metrics, [`child${i}`]) + child.value = i * 10 + } + + const aggregated = metricsAggregate(metrics, 3) + + expect(Object.keys(aggregated.measurements).length).toBe(3) + }) + + it('should filter out metrics starting with #', () => { + const metrics = newMetrics() + + const child1 = childMetrics(metrics, ['normal']) + child1.value = 50 + + const child2 = childMetrics(metrics, ['#internal']) + child2.value = 30 + + const aggregated = metricsAggregate(metrics) + + expect(aggregated.value).toBe(50) // only 'normal' counted + }) + }) + + describe('metricsToString', () => { + it('should convert metrics to string', () => { + const metrics = newMetrics() + metrics.value = 100 + metrics.operations = 10 + + const str = metricsToString(metrics, 'TestMetrics', 50) + + expect(str).toContain('TestMetrics') + expect(str).toContain('100') + expect(str).toContain('10') + }) + + it('should include child metrics in string', () => { + const metrics = newMetrics() + const child = childMetrics(metrics, ['operation']) + child.value = 50 + child.operations = 5 + + const str = metricsToString(metrics, 'TestMetrics', 50) + + expect(str).toContain('operation') + }) + }) + + describe('metricsToJson', () => { + it('should convert metrics to JSON', () => { + const metrics = newMetrics() + metrics.value = 100 + metrics.operations = 10 + + const json = metricsToJson(metrics) + + // aggregated value is the total value when no children + expect(json.$total).toBe(100) + expect(json.$ops).toBe(10) + }) + + it('should include child metrics in JSON', () => { + const metrics = newMetrics() + const child = childMetrics(metrics, ['operation']) + child.value = 50 + child.operations = 5 + + const json = metricsToJson(metrics) + + expect(json).toBeDefined() + const keys = Object.keys(json) + expect(keys.some((k) => k.includes('operation'))).toBe(true) + }) + }) + + describe('metricsToRows', () => { + it('should convert metrics to rows', () => { + const metrics = newMetrics() + metrics.value = 100 + metrics.operations = 10 + + const rows = metricsToRows(metrics, 'TestMetrics') + + expect(Array.isArray(rows)).toBe(true) + expect(rows.length).toBeGreaterThan(0) + expect(rows[0]).toContain('TestMetrics') + }) + + it('should include child metrics in rows', () => { + const metrics = newMetrics() + const child = childMetrics(metrics, ['operation']) + child.value = 50 + child.operations = 5 + + const rows = metricsToRows(metrics, 'TestMetrics') + + expect(rows.length).toBeGreaterThan(1) + expect(rows.some((row) => row.includes('operation'))).toBe(true) + }) + + it('should properly format row values', () => { + const metrics = newMetrics() + metrics.value = 100 + metrics.operations = 10 + + const rows = metricsToRows(metrics, 'TestMetrics') + + expect(rows[0]).toHaveLength(5) // offset, name, avg, total, ops + expect(typeof rows[0][0]).toBe('number') // offset + expect(typeof rows[0][1]).toBe('string') // name + }) + }) +}) diff --git a/foundations/core/packages/measurements/src/__tests__/performance.test.ts b/foundations/core/packages/measurements/src/__tests__/performance.test.ts new file mode 100644 index 0000000000..14f0ff3e35 --- /dev/null +++ b/foundations/core/packages/measurements/src/__tests__/performance.test.ts @@ -0,0 +1,260 @@ +import { MeasureMetricsContext, NoMetricsContext, noParamsLogger } from '../context' +import { newMetrics, metricsAggregate } from '../metrics' +import type { MeasureContext } from '../types' + +describe('performance', () => { + describe('overhead measurement', () => { + // Reduced iterations to fit within 10 seconds total test time + const iterations = 100 + const depth = 5 + + it('should measure overhead of with() vs raw execution', async () => { + // Baseline: raw execution without measurement + const baselineStart = performance.now() + for (let i = 0; i < iterations; i++) { + await simulateWork(1) + } + const baselineTime = performance.now() - baselineStart + + // With measurement context + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger) + + const measuredStart = performance.now() + for (let i = 0; i < iterations; i++) { + await ctx.with('operation', { iteration: i }, async () => { + await simulateWork(1) + }) + } + const measuredTime = performance.now() - measuredStart + + const overhead = measuredTime - baselineTime + const overheadPercentage = (overhead / baselineTime) * 100 + + console.log(`\n📊 Overhead Analysis (${iterations} iterations):`) + console.log(` Baseline time: ${baselineTime.toFixed(2)}ms`) + console.log(` Measured time: ${measuredTime.toFixed(2)}ms`) + console.log(` Overhead: ${overhead.toFixed(2)}ms (${overheadPercentage.toFixed(2)}%)`) + console.log(` Per operation: ${(overhead / iterations).toFixed(4)}ms`) + + expect(metrics.measurements.operation).toBeDefined() + expect(metrics.measurements.operation.operations).toBe(iterations) + + // Overhead should be reasonable (typically < 50% for simple operations) + // This is informational rather than a strict assertion + expect(overheadPercentage).toBeLessThan(200) + }) + + it('should measure overhead with deep nested contexts', async () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger) + + // Baseline: single level + const singleLevelStart = performance.now() + for (let i = 0; i < iterations; i++) { + await ctx.with('shallow', {}, async () => { + await simulateWork(1) + }) + } + const singleLevelTime = performance.now() - singleLevelStart + + // Deep nesting + const deepStart = performance.now() + for (let i = 0; i < iterations; i++) { + await deepNestedExecution(ctx, depth, 1) + } + const deepTime = performance.now() - deepStart + + const nestingOverhead = deepTime - singleLevelTime + const overheadPerLevel = nestingOverhead / (iterations * depth) + + console.log(`\n📊 Deep Nesting Overhead (${iterations} iterations, depth=${depth}):`) + console.log(` Single level: ${singleLevelTime.toFixed(2)}ms`) + console.log(` Deep nested: ${deepTime.toFixed(2)}ms`) + console.log(` Nesting overhead: ${nestingOverhead.toFixed(2)}ms`) + console.log(` Per level: ${overheadPerLevel.toFixed(4)}ms`) + + expect(metrics.measurements.shallow).toBeDefined() + expect(metrics.measurements.level0).toBeDefined() + }) + + it('should measure overhead with complex parameter tracking', async () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger) + + // Simple params + const simpleStart = performance.now() + for (let i = 0; i < iterations; i++) { + await ctx.with('simple', { id: i }, async () => { + await simulateWork(1) + }) + } + const simpleTime = performance.now() - simpleStart + + // Complex params with multiple tracked values + const complexStart = performance.now() + for (let i = 0; i < iterations; i++) { + await ctx.with( + 'complex', + { + method: i % 3 === 0 ? 'GET' : i % 3 === 1 ? 'POST' : 'PUT', + status: i % 2 === 0 ? 200 : 404, + cached: i % 4 === 0 + }, + async () => { + await simulateWork(1) + }, + { + userId: `user_${i % 10}`, + endpoint: `/api/v1/resource/${i % 5}`, + timestamp: Date.now() + } + ) + } + const complexTime = performance.now() - complexStart + + const paramOverhead = complexTime - simpleTime + + console.log(`\n📊 Parameter Tracking Overhead (${iterations} iterations):`) + console.log(` Simple params: ${simpleTime.toFixed(2)}ms`) + console.log(` Complex params: ${complexTime.toFixed(2)}ms`) + console.log(` Param overhead: ${paramOverhead.toFixed(2)}ms`) + console.log(` Per operation: ${(paramOverhead / iterations).toFixed(4)}ms`) + + expect(metrics.measurements.simple).toBeDefined() + expect(metrics.measurements.complex).toBeDefined() + expect(Object.keys(metrics.measurements.complex.params).length).toBeGreaterThan(0) + }) + + it('should measure overhead with NoMetricsContext', async () => { + // NoMetricsContext should have minimal overhead + const noMetricsCtx = new NoMetricsContext(noParamsLogger) + + const start = performance.now() + for (let i = 0; i < iterations; i++) { + await noMetricsCtx.with('operation', { iteration: i }, async () => { + await simulateWork(1) + }) + } + const noMetricsTime = performance.now() - start + + // Compare with raw execution + const rawStart = performance.now() + for (let i = 0; i < iterations; i++) { + await simulateWork(1) + } + const rawTime = performance.now() - rawStart + + const overhead = noMetricsTime - rawTime + const overheadPercentage = (overhead / rawTime) * 100 + + console.log(`\n📊 NoMetricsContext Overhead (${iterations} iterations):`) + console.log(` Raw time: ${rawTime.toFixed(2)}ms`) + console.log(` NoMetrics time: ${noMetricsTime.toFixed(2)}ms`) + console.log(` Overhead: ${overhead.toFixed(2)}ms (${overheadPercentage.toFixed(2)}%)`) + + // NoMetricsContext should have very low overhead + expect(overheadPercentage).toBeLessThan(50) + }) + }) + + describe('realistic workload simulation', () => { + it('should measure overhead in complex realistic scenario', async () => { + const metrics = newMetrics() + const ctx = new MeasureMetricsContext('app', { service: 'api' }, {}, metrics, noParamsLogger) + + const requests = 50 + const baselineStart = performance.now() + + // Simulate without metrics + for (let i = 0; i < requests; i++) { + await simulateAPIRequest(null, i) + } + const baselineTime = performance.now() - baselineStart + + // Reset for measured run + const measuredStart = performance.now() + + // Simulate with metrics + for (let i = 0; i < requests; i++) { + await simulateAPIRequest(ctx, i) + } + const measuredTime = performance.now() - measuredStart + + const overhead = measuredTime - baselineTime + const overheadPercentage = (overhead / baselineTime) * 100 + + console.log(`\n📊 Realistic Workload Analysis (${requests} API requests):`) + console.log(` Baseline: ${baselineTime.toFixed(2)}ms`) + console.log(` With metrics: ${measuredTime.toFixed(2)}ms`) + console.log(` Overhead: ${overhead.toFixed(2)}ms (${overheadPercentage.toFixed(2)}%)`) + console.log(` Per request: ${(overhead / requests).toFixed(4)}ms`) + + // Check metrics structure + const aggregated = metricsAggregate(metrics, 10) + console.log(` Collected operations: ${aggregated.measurements.request?.operations ?? 0}`) + + expect(aggregated.measurements.request).toBeDefined() + expect(overheadPercentage).toBeLessThan(100) // Should be less than 100% overhead + }) + }) +}) + +// Helper functions + +async function simulateWork (durationMs: number): Promise { + const end = performance.now() + durationMs + while (performance.now() < end) { + // Busy wait to simulate work + const r = Math.random() * Math.random() + expect(r).toBeGreaterThanOrEqual(0) + } +} + +async function deepNestedExecution ( + ctx: MeasureContext, + depth: number, + workMs: number, + currentLevel: number = 0 +): Promise { + if (currentLevel >= depth) { + await simulateWork(workMs) + return + } + + await ctx.with(`level${currentLevel}`, { level: currentLevel }, async (childCtx) => { + await deepNestedExecution(childCtx, depth, workMs, currentLevel + 1) + }) +} + +async function simulateAPIRequest (ctx: MeasureContext | null, requestId: number): Promise { + const method = ['GET', 'POST', 'PUT'][requestId % 3] + const endpoint = `/api/resource/${requestId % 10}` + + if (ctx === null) { + // No metrics version + await simulateWork(1) + // Simulate DB query + await simulateWork(2) + // Simulate processing + await simulateWork(1) + return + } + + // With metrics version + await ctx.with('request', { method, endpoint }, async (reqCtx) => { + await reqCtx.with('auth', { userId: `user_${requestId % 50}` }, async () => { + await simulateWork(1) + }) + + await reqCtx.with('database', { query: 'SELECT' }, async (dbCtx) => { + await dbCtx.with('query_execution', {}, async () => { + await simulateWork(2) + }) + }) + + await reqCtx.with('processing', { items: requestId % 20 }, async () => { + await simulateWork(1) + }) + }) +} diff --git a/foundations/core/packages/measurements/src/context.ts b/foundations/core/packages/measurements/src/context.ts new file mode 100644 index 0000000000..62dbb347cc --- /dev/null +++ b/foundations/core/packages/measurements/src/context.ts @@ -0,0 +1,392 @@ +// Basic performance metrics suite. + +import { platformNow, platformNowDiff } from '.' +import { childMetrics, newMetrics, updateMeasure } from './metrics' +import { + type FullParamsType, + type MeasureContext, + type MeasureLogger, + type Metrics, + type ParamsType, + type OperationLog, + type OperationLogEntry, + type WithOptions +} from './types' + +const errorPrinter = ({ message, stack, ...rest }: Error): object => ({ + message, + stack, + ...rest +}) +function replacer (value: any): any { + return value instanceof Error ? errorPrinter(value) : value +} + +export const consoleLogger = (logParams: Record): MeasureLogger => ({ + info: (msg, args) => { + console.info( + msg, + ...Object.entries({ ...(args ?? {}), ...(logParams ?? {}) }).map( + (it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}` + ) + ) + }, + error: (msg, args) => { + console.error( + msg, + ...Object.entries({ ...(args ?? {}), ...(logParams ?? {}) }).map( + (it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}` + ) + ) + }, + warn: (msg, args) => { + console.warn(msg, ...Object.entries(args ?? {}).map((it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}`)) + }, + close: async () => {}, + logOperation: (operation, time, params) => {} +}) + +export const noParamsLogger = consoleLogger({}) + +export const nullPromise = Promise.resolve() + +/** + * @public + */ +export class MeasureMetricsContext implements MeasureContext { + private readonly name: string + private readonly params: ParamsType + + private readonly fullParams: FullParamsType | (() => FullParamsType) = {} + logger: MeasureLogger + metrics: Metrics + id?: string + + st = platformNow() + contextData: object = {} + private done (value?: number, override?: boolean): void { + updateMeasure(this.metrics, this.st, this.params, this.fullParams, (spend) => {}, value, override) + } + + constructor ( + name: string, + params: ParamsType, + fullParams: FullParamsType | (() => FullParamsType) = {}, + metrics: Metrics = newMetrics(), + logger?: MeasureLogger, + readonly parent?: MeasureContext, + readonly logParams?: ParamsType + ) { + this.name = name + this.params = params + this.fullParams = fullParams + this.metrics = metrics + this.metrics.namedParams = this.metrics.namedParams ?? {} + for (const [k, v] of Object.entries(params)) { + if (this.metrics.namedParams[k] !== v) { + this.metrics.namedParams[k] = v + } else { + this.metrics.namedParams[k] = '*' + } + } + + this.logger = logger ?? (this.logParams != null ? consoleLogger(this.logParams ?? {}) : noParamsLogger) + } + + measure (name: string, value: number, override?: boolean): void { + const c = new MeasureMetricsContext('#' + name, {}, {}, childMetrics(this.metrics, ['#' + name]), this.logger, this) + c.contextData = this.contextData + c.done(value, override) + } + + newChild ( + name: string, + params: ParamsType, + opt?: { + fullParams?: FullParamsType + logger?: MeasureLogger + span?: WithOptions['span'] // By default true + } + ): MeasureContext { + const result = new MeasureMetricsContext( + name, + params, + opt?.fullParams ?? {}, + childMetrics(this.metrics, [name]), + opt?.logger ?? this.logger, + this, + this.logParams + ) + result.id = this.id + result.contextData = this.contextData + return result + } + + with( + name: string, + params: ParamsType, + op: (ctx: MeasureContext) => T | Promise, + fullParams?: ParamsType | (() => FullParamsType), + opt?: WithOptions + ): Promise { + const c = this.newChild(name, params, { fullParams, logger: this.logger }) + let needFinally = true + try { + const value = op(c) + if (value instanceof Promise) { + needFinally = false + return value.finally(() => { + c.end() + if (opt?.log === true) { + this.logger.logOperation(name, platformNowDiff((c as MeasureMetricsContext).st), { + ...params, + ...fullParams + }) + } + }) + } else { + if (value == null) { + return nullPromise as Promise + } + return Promise.resolve(value) + } + } finally { + if (needFinally) { + c.end() + } + } + } + + extractMeta (): Record { + return {} + } + + withSync( + name: string, + params: ParamsType, + op: (ctx: MeasureContext) => T, + fullParams?: ParamsType | (() => FullParamsType) + ): T { + const c = this.newChild(name, params, { fullParams, logger: this.logger }) + try { + return op(c) + } finally { + c.end() + } + } + + error (message: string, args?: Record): void { + this.logger.error(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) + } + + info (message: string, args?: Record): void { + this.logger.info(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) + } + + warn (message: string, args?: Record): void { + this.logger.warn(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) + } + + end (): void { + this.done() + } + + getParams (): ParamsType { + return this.params + } +} + +export class NoMetricsContext implements MeasureContext { + logger: MeasureLogger + id?: string + + contextData: object = {} + + constructor (logger?: MeasureLogger) { + this.logger = logger ?? consoleLogger({}) + } + + measure (name: string, value: number, override?: boolean): void {} + + newChild ( + name: string, + params: ParamsType, + fullParams?: FullParamsType | (() => FullParamsType), + logger?: MeasureLogger + ): MeasureContext { + const result = new NoMetricsContext(logger ?? this.logger) + result.id = this.id + result.contextData = this.contextData + return result + } + + with( + name: string, + params: ParamsType, + op: (ctx: MeasureContext) => T | Promise, + fullParams?: ParamsType | (() => FullParamsType) + ): Promise { + const r = op(this.newChild(name, params, fullParams, this.logger)) + return r instanceof Promise ? r : Promise.resolve(r) + } + + extractMeta (): Record { + return {} + } + + withSync( + name: string, + params: ParamsType, + op: (ctx: MeasureContext) => T, + fullParams?: ParamsType | (() => FullParamsType) + ): T { + const c = this.newChild(name, params, fullParams, this.logger) + return op(c) + } + + withLog( + name: string, + params: ParamsType, + op: (ctx: MeasureContext) => T | Promise, + fullParams?: ParamsType + ): Promise { + const r = op(this.newChild(name, params, fullParams, this.logger)) + return r instanceof Promise ? r : Promise.resolve(r) + } + + error (message: string, args?: Record): void { + this.logger.error(message, { ...args }) + } + + info (message: string, args?: Record): void { + this.logger.info(message, { ...args }) + } + + warn (message: string, args?: Record): void { + this.logger.warn(message, { ...args }) + } + + end (): void {} + + getParams (): ParamsType { + return {} + } +} + +/** + * Allow to use decorator for context enabled functions + */ +export function withContext (name: string, params: ParamsType = {}, options?: WithOptions): any { + return (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor => { + const originalMethod = descriptor.value + descriptor.value = function (...args: any[]): Promise { + const ctx = args[0] as MeasureContext + return ctx.with( + name, + params, + (ctx) => originalMethod.apply(this, [ctx, ...args.slice(1)]) as Promise, + {}, + options + ) + } + return descriptor + } +} + +let operationProfiling = false + +export function setOperationLogProfiling (value: boolean): void { + operationProfiling = value +} + +let globalId: number = 0 + +export function registerOperationLog (ctx: MeasureContext): { opLogMetrics?: Metrics, op?: OperationLog } { + if (!operationProfiling) { + return {} + } + const op: OperationLog = { start: platformNow(), ops: [], end: -1 } + let opLogMetrics: Metrics | undefined + + if (ctx.id === undefined) { + ctx.id = 'op_' + (++globalId).toString(16) + } + if (ctx.metrics !== undefined) { + if (ctx.metrics.opLog === undefined) { + ctx.metrics.opLog = {} + } + ctx.metrics.opLog[ctx.id] = op + opLogMetrics = ctx.metrics + } + return { opLogMetrics, op } +} + +export function updateOperationLog (opLogMetrics: Metrics | undefined, op: OperationLog | undefined): void { + if (!operationProfiling) { + return + } + if (op !== undefined) { + op.end = platformNow() + } + // We should keep only longest one entry + if (opLogMetrics?.opLog !== undefined) { + const entries = Object.entries(opLogMetrics.opLog) + + const incomplete = entries.filter((it) => it[1].end === -1) + const complete = entries.filter((it) => it[1].end !== -1) + complete.sort((a, b) => a[1].start - b[1].start) + if (complete.length > 30) { + complete.splice(0, complete.length - 30) + } + + opLogMetrics.opLog = Object.fromEntries(incomplete.concat(complete)) + } +} + +export function addOperation ( + ctx: MeasureContext, + name: string, + params: ParamsType, + op: (ctx: MeasureContext) => Promise, + fullParams?: FullParamsType +): Promise { + if (!operationProfiling) { + return op(ctx) + } + let opEntry: OperationLogEntry | undefined + + let p: MeasureContext | undefined = ctx + let opLogMetrics: Metrics | undefined + let id: string | undefined + + while (p !== undefined) { + if (p.metrics?.opLog !== undefined) { + opLogMetrics = p.metrics + } + if (id === undefined && p.id !== undefined) { + id = p.id + } + p = p.parent + } + const opLog = id !== undefined ? opLogMetrics?.opLog?.[id] : undefined + + if (opLog !== undefined) { + opEntry = { + op: name, + start: performance.now(), + params: {}, + end: -1 + } + } + const result = op(ctx) + if (opEntry !== undefined && opLog !== undefined) { + void result.finally(() => { + if (opEntry !== undefined && opLog !== undefined) { + opEntry.end = performance.now() + opEntry.params = { ...params, ...(typeof fullParams === 'function' ? fullParams() : fullParams) } + opLog.ops.push(opEntry) + } + }) + } + return result +} diff --git a/foundations/core/packages/measurements/src/index.ts b/foundations/core/packages/measurements/src/index.ts new file mode 100644 index 0000000000..16e4d95df6 --- /dev/null +++ b/foundations/core/packages/measurements/src/index.ts @@ -0,0 +1,13 @@ +export * from './context' +export * from './metrics' +export type * from './types' + +/** + * Return a current performance timestamp + */ +export const platformNow: () => number = () => performance.now() + +/** + * Return a diff with previous performance snapshot with 2 digits after . max. + */ +export const platformNowDiff = (old: number): number => Math.round((performance.now() - old) * 100) / 100 diff --git a/foundations/core/packages/measurements/src/metrics.ts b/foundations/core/packages/measurements/src/metrics.ts new file mode 100644 index 0000000000..695dc0372a --- /dev/null +++ b/foundations/core/packages/measurements/src/metrics.ts @@ -0,0 +1,343 @@ +// Basic performance metrics suite. + +import { platformNow, type MetricsData } from '.' +import { type FullParamsType, type Metrics, type ParamsType } from './types' + +/** + * @public + */ +export const globals: Metrics = newMetrics() + +/** + * @public + * @returns + */ +export function newMetrics (): Metrics { + return { + operations: 0, + value: 0, + measurements: {}, + params: {}, + namedParams: {} + } +} + +function getUpdatedTopResult ( + current: Metrics['topResult'], + time: number, + params: FullParamsType +): Metrics['topResult'] { + if (time === 0) { + return current + } + const result: Metrics['topResult'] = current ?? [] + + const newValue = { + value: time, + params + } + + if (result.length > 6) { + if (result[0].value < newValue.value) { + result[0] = newValue + return result + } + if (result[result.length - 1].value > newValue.value) { + result[result.length - 1] = newValue + return result + } + + // Shift the middle + return [result[0], newValue, ...result.slice(1, 3), result[5]] + } else { + result.push(newValue) + return result + } +} + +/** + * Measure with tree expansion. Operation counter will be added only to leaf's. + * @public + */ +export function measure ( + metrics: Metrics, + params: ParamsType, + fullParams: FullParamsType | (() => FullParamsType) = {}, + endOp?: (spend: number) => void +): () => void { + const st = platformNow() + return () => { + updateMeasure(metrics, st, params, fullParams, endOp) + } +} +export function updateMeasure ( + metrics: Metrics, + st: number, + params: ParamsType, + fullParams: FullParamsType | (() => FullParamsType), + endOp?: (spend: number) => void, + value?: number, + override?: boolean +): void { + const ed = platformNow() + + const fParams = typeof fullParams === 'function' ? fullParams() : fullParams + // Update params if required + const pparams = Object.entries(params) + if (pparams.length > 0) { + const [k, v] = pparams[0] + let params = metrics.params[k] + if (params === undefined) { + params = {} + metrics.params[k] = params + } + const vKey = `${v?.toString() ?? ''}` + let param = params[vKey] + if (param === undefined) { + param = { + operations: 0, + value: 0 + } + params[vKey] = param + } + if (override === true) { + if (value === 0) { + // We need to delete value, to preserve sending zero values. + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete params[vKey] + } else { + param.operations = value ?? ed - st + } + } else { + param.value += value ?? ed - st + param.operations++ + } + // Do not update top results for params. + if (pparams.length > 1) { + // We need to update all other params as counters. + if (param.topResult === undefined) { + param.topResult = [] + } + for (const [, v] of pparams.slice(1)) { + const r = (param.topResult ?? []).find((it) => it.params[`${v}`] === true) + if (r !== undefined) { + r.value += 1 // Counter of operations + r.time = (r.time ?? 0) + (value ?? ed - st) + } else { + param.topResult.push({ params: { [`${v}`]: true }, value: 1, time: value ?? ed - st }) + } + } + param.topResult.sort((a, b) => b.value - a.value) + } + } + // Update leaf data + if (override === true) { + metrics.operations = value ?? ed - st + } else { + metrics.value += value ?? ed - st + metrics.operations++ + } + + metrics.topResult = getUpdatedTopResult(metrics.topResult, ed - st, fParams) + endOp?.(ed - st) +} + +/** + * @public + */ +export function childMetrics (root: Metrics, path: string[]): Metrics { + const segments = path + let oop = root + for (const p of segments) { + const v = oop.measurements[p] ?? { operations: 0, value: 0, measurements: {}, params: {} } + oop.measurements[p] = v + oop = v + } + return oop +} + +/** + * @public + */ +export function metricsAggregate (m: Metrics, limit: number = -1, roundMath: boolean = false): Metrics { + let ms = aggregateMetrics(m.measurements, limit) + + // Use child overage, if there is no top level value specified. + const me = Object.entries(ms) + const sumVal: number = + (me.length === 0 ? m.value : 0) + + me + .filter((it) => !it[0].startsWith('#')) + .map((it) => it[1]) + .reduce((p, v) => { + return p + v.value + }, 0) + + if (limit !== -1) { + // We need to keep only top limit items in ms + if (Object.keys(ms).length > 0) { + const newMs: typeof ms = {} + let added = 0 + for (const [k, v] of Object.entries(ms)) { + newMs[k] = v + added++ + if (added >= limit) { + break + } + } + ms = newMs + } + } + + return { + operations: m.operations, + measurements: ms, + params: m.params, + value: sumVal, + topResult: m.topResult, + namedParams: m.namedParams, + opLog: m.opLog + } +} + +function aggregateMetrics (m: Record, limit: number = -1): Record { + const result: Record = {} + for (const [k, v] of Object.entries(m).sort((a, b) => b[1].value - a[1].value)) { + result[k] = metricsAggregate(v, limit) + } + return result +} + +function toLen (val: string, sep: string, len: number): string { + while (val.length < len) { + val += sep + } + return val +} + +function printMetricsChildren (params: Record, offset: number, length: number): string { + let r = '' + if (Object.keys(params).length > 0) { + r += '\n' + toLen('', ' ', offset) + r += Object.entries(params) + .filter((it) => it[1].value > 0.1) + .map(([k, vv]) => toString(k, vv, offset, length)) + .join('\n' + toLen('', ' ', offset)) + } + return r +} + +function printMetricsParams ( + params: Record>, + offset: number, + length: number +): string { + let r = '' + const joinP = (key: string, data: Record): string[] => { + return Object.entries(data) + .filter((it) => it[1].value >= 0.1) + .map(([k, vv]) => + `${toLen('', ' ', offset)}${toLen(key + '=' + k, '-', length - offset)}: avg ${ + Math.round((vv.value / (vv.operations > 0 ? vv.operations : 1)) * 100) / 100 + } total: ${Math.round(vv.value * 100) / 100} ops: ${vv.operations}`.trim() + ) + } + const joinParams = Object.entries(params).reduce((p, c) => [...p, ...joinP(c[0], c[1])], []) + if (Object.keys(joinParams).length > 0) { + r += '\n' + toLen('', ' ', offset) + r += joinParams.join('\n' + toLen('', ' ', offset)) + } + return r +} + +function toString (name: string, m: Metrics, offset: number, length: number): string { + let r = `${toLen('', ' ', offset)}${toLen(name, '-', length - offset)}: avg ${ + Math.round((m.value / (m.operations > 0 ? m.operations : 1)) * 100) / 100 + } total: ${Math.round(m.value * 100) / 100} ops: ${m.operations}`.trim() + r += printMetricsParams(m.params, offset + 4, length) + r += printMetricsChildren(m.measurements, offset + 4, length) + return r +} + +function toJson (m: Metrics): any { + const obj: any = { + $total: m.value, + $ops: m.operations + } + if (m.operations > 1) { + obj.avg = Math.round((m.value / (m.operations > 0 ? m.operations : 1)) * 100) / 100 + } + if (Object.keys(m.params).length > 0) { + obj.params = m.params + } + for (const [k, v] of Object.entries(m.measurements ?? {})) { + obj[ + `${k} ${v.value} ${v.operations} ${ + v.operations > 1 ? Math.round((v.value / (v.operations > 0 ? m.operations : 1)) * 100) / 100 : '' + }` + ] = toJson(v) + } + + return obj +} + +/** + * @public + */ +export function metricsToString (metrics: Metrics, name = 'System', length: number): string { + return toString(name, metricsAggregate(metrics, 50, true), 0, length) +} + +export function metricsToJson (metrics: Metrics): any { + return toJson(metricsAggregate(metrics)) +} + +function printMetricsParamsRows ( + params: Record>, + offset: number +): (string | number)[][] { + const r: (string | number)[][] = [] + function joinP (key: string, data: Record): (string | number)[][] { + return Object.entries(data).map(([k, vv]) => [ + offset, + `${key}=${k}`, + Math.round((vv.value / (vv.operations > 0 ? vv.operations : 1)) * 100) / 100, + Math.round(vv.value * 100) / 100, + vv.operations + ]) + } + for (const [k, v] of Object.entries(params)) { + r.push(...joinP(k, v)) + } + return r +} + +function printMetricsChildrenRows (params: Record, offset: number): (string | number)[][] { + const r: (string | number)[][] = [] + if (Object.keys(params).length > 0) { + Object.entries(params).forEach(([k, vv]) => r.push(...toStringRows(k, vv, offset))) + } + return r +} + +function toStringRows (name: string, m: Metrics, offset: number): (number | string)[][] { + const r: (number | string)[][] = [ + [ + offset, + name, + Math.round((m.value / (m.operations > 0 ? m.operations : 1)) * 100) / 100, + Math.round(m.value * 100) / 100, + m.operations + ] + ] + r.push(...printMetricsParamsRows(m.params, offset + 1)) + r.push(...printMetricsChildrenRows(m.measurements, offset + 1)) + return r +} + +/** + * @public + */ +export function metricsToRows (metrics: Metrics, name = 'System'): (number | string)[][] { + return toStringRows(name, metricsAggregate(metrics, 50, true), 0) +} diff --git a/foundations/core/packages/measurements/src/types.ts b/foundations/core/packages/measurements/src/types.ts new file mode 100644 index 0000000000..d2d2e1366b --- /dev/null +++ b/foundations/core/packages/measurements/src/types.ts @@ -0,0 +1,135 @@ +/** + * @public + */ +export type ParamType = string | number | boolean | undefined + +/** + * @public + */ +export type ParamsType = Record + +/** + * @public + */ +export type FullParamsType = Record + +/** + * @public + */ +export interface MetricsData { + operations: number + value: number + topResult?: { + value: number + time?: number + params: FullParamsType + }[] +} + +export interface OperationLogEntry { + op: string + params: ParamsType + start: number + end: number +} +export interface OperationLog { + ops: OperationLogEntry[] + start: number + end: number +} + +/** + * @public + */ +export interface Metrics extends MetricsData { + namedParams: ParamsType + params: Record> + measurements: Record + + opLog?: Record +} + +/** + * @public + */ +export interface MeasureLogger { + info: (message: string, obj?: Record) => void + error: (message: string, obj?: Record) => void + + warn: (message: string, obj?: Record) => void + + logOperation: (operation: string, time: number, params: ParamsType) => void + + childLogger?: (name: string, params: Record) => MeasureLogger + + close: () => Promise +} + +export interface WithOptions { + span?: true | false | 'disable' | 'skip' | 'inherit' // 'none' means no span will be created, 'disable' means context will be tracing disabled + log?: boolean + inheritParams?: boolean + + // Passed context metadata + meta?: Record + + // If passed, will not send an error into span, for some cases we need to throw error from with, without reporting it. + suspendErrors?: boolean +} + +/** + * @public + */ +export interface MeasureContext { + id?: string + + // Context data will be copied referenced for all child contexts. + contextData: Q + // Create a child metrics context + newChild: ( + name: string, + params: ParamsType, + opt?: { + fullParams?: FullParamsType + logger?: MeasureLogger + span?: WithOptions['span'] // By default true + meta?: Record + } + ) => MeasureContext + + metrics?: Metrics + + with: ( + name: string, + params: ParamsType, + op: (ctx: MeasureContext) => T | Promise, + fullParams?: FullParamsType | (() => FullParamsType), + opt?: WithOptions + ) => Promise + + withSync: ( + name: string, + params: ParamsType, + op: (ctx: MeasureContext) => T, + fullParams?: FullParamsType | (() => FullParamsType), + opt?: WithOptions + ) => T + + extractMeta: () => Record + + logger: MeasureLogger + + parent?: MeasureContext + getParams: () => ParamsType + + measure: (name: string, value: number, override?: boolean) => void + + // Capture error + error: (message: string, obj?: Record) => void + info: (message: string, obj?: Record) => void + warn: (message: string, obj?: Record) => void + + // Mark current context as complete + // If no value is passed, time difference will be used. + end: (value?: number) => void +} diff --git a/foundations/core/packages/measurements/tsconfig.json b/foundations/core/packages/measurements/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/measurements/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/model/.eslintrc.js b/foundations/core/packages/model/.eslintrc.js new file mode 100644 index 0000000000..ce90fb9646 --- /dev/null +++ b/foundations/core/packages/model/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/model/.npmignore b/foundations/core/packages/model/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/model/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/model/CHANGELOG.json b/foundations/core/packages/model/CHANGELOG.json new file mode 100644 index 0000000000..5fbed02733 --- /dev/null +++ b/foundations/core/packages/model/CHANGELOG.json @@ -0,0 +1,127 @@ +{ + "name": "@hcengineering/model", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/model_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "none": [ + { + "comment": "formatting" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.6", + "tag": "@hcengineering/model_v0.7.6", + "date": "Tue, 21 Oct 2025 19:04:55 GMT", + "comments": { + "patch": [ + { + "comment": "Add TypeIdentifier for custom attribute incremental IDs" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.8` to `0.7.10`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/model_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/storage\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/rank\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/account-client\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/model_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/storage\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/rank\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/account-client\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/model_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/model_v0.6.0", + "date": "Sun, 08 Aug 2021 10:14:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.3` to `~0.6.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/model/CHANGELOG.md b/foundations/core/packages/model/CHANGELOG.md new file mode 100644 index 0000000000..5dbecb05d8 --- /dev/null +++ b/foundations/core/packages/model/CHANGELOG.md @@ -0,0 +1,40 @@ +# Change Log - @hcengineering/model + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.6 +Tue, 21 Oct 2025 19:04:55 GMT + +### Patches + +- Add TypeIdentifier for custom attribute incremental IDs + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Version update only_ + +## 0.7.0 +Sun, 08 Aug 2021 10:14:57 GMT + +_Initial release_ + diff --git a/foundations/core/packages/model/config/rig.json b/foundations/core/packages/model/config/rig.json new file mode 100644 index 0000000000..78cc5a1733 --- /dev/null +++ b/foundations/core/packages/model/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "node" +} diff --git a/foundations/core/packages/model/jest.config.js b/foundations/core/packages/model/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/model/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/model/package.json b/foundations/core/packages/model/package.json new file mode 100644 index 0000000000..feaba7885c --- /dev/null +++ b/foundations/core/packages/model/package.json @@ -0,0 +1,68 @@ +{ + "name": "@hcengineering/model", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Anticrm Platform Contributors", + "template": "@hcengineering/node-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent --forceExit --coverage", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --forceExit --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@types/toposort": "^2.0.3", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/node": "^22.18.1", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/platform": "workspace:^0.7.18", + "@hcengineering/storage": "workspace:^0.7.17", + "@hcengineering/analytics": "workspace:^0.7.17", + "@hcengineering/rank": "workspace:^0.7.17", + "@hcengineering/account-client": "workspace:^0.7.19", + "toposort": "^2.0.2", + "fast-equals": "^5.2.2" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "files": [ + "lib/**/*", + "!lib/**/__test__/**", + "types/**/*", + "!types/**/__test__/**", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/model/src/dsl.ts b/foundations/core/packages/model/src/dsl.ts new file mode 100644 index 0000000000..dfe0313ae1 --- /dev/null +++ b/foundations/core/packages/model/src/dsl.ts @@ -0,0 +1,544 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { + type AccountUuid, + type AttachedDoc, + type Attribute, + type Class, + type Classifier, + ClassifierKind, + type CustomSequence, + type Data, + DateRangeMode, + type Doc, + type Domain, + type Enum, + type EnumOf, + Hierarchy, + type Hyperlink, + type Mixin as IMixin, + type IndexKind, + type Interface, + type Markup, + type MarkupBlobRef, + type MixinData, + type Obj, + type PersonId, + type PropertyType, + type Rank, + type Ref, + type RefTo, + type Space, + type Timestamp, + type Tx, + type TxCreateDoc, + TxFactory, + TxProcessor, + type Type, + type TypeAny as TypeAnyType, + type ArrOf as TypeArrOf, + type Collection as TypeCollection, + type TypeDate as TypeDateType, + type TypeIdentifier as TypeIdentifierType, + type TypeNumber as TypeNumberType, + generateId +} from '@hcengineering/core' +import type { Asset, IntlString } from '@hcengineering/platform' +import toposort from 'toposort' + +const targets = new Map>() + +function setIndex (target: any, property: string, index: IndexKind): void { + let indexes = targets.get(target) + if (indexes === undefined) { + indexes = new Map() + targets.set(target, indexes) + } + indexes.set(property, index) +} + +function getIndex (target: any, property: string): IndexKind | undefined { + return targets.get(target)?.get(property) +} + +interface ClassTxes { + _id: Ref + extends?: Ref> + implements?: Ref>[] + domain?: Domain + label: IntlString + icon?: Asset + txes: Array + kind: ClassifierKind + shortLabel?: string | IntlString + sortingKey?: string + filteringKey?: string + pluralLabel?: IntlString +} + +const transactions = new Map() + +function getTxes (target: any): ClassTxes { + const txes = transactions.get(target) + if (txes === undefined) { + const txes = { txes: [] } as unknown as ClassTxes + transactions.set(target, txes) + return txes + } + return txes +} + +const attributes = new Map>>() +function setAttr (target: any, prop: string, key: string, value: any): void { + const props = attributes.get(target) ?? new Map>() + const attrs = props.get(prop) ?? {} + attrs[key] = value + + props.set(prop, attrs) + attributes.set(target, props) +} + +function clearAttrs (target: any, prop: string): void { + const props = attributes.get(target) + props?.delete(prop) + + if (props !== undefined && props.size === 0) { + attributes.delete(target) + } +} + +function getAttrs (target: any, prop: string): Record { + return attributes.get(target)?.get(prop) ?? {} +} + +/** + * @public + */ +export function Prop (type: Type, label: IntlString, extra: Partial> = {}) { + return function (target: any, propertyKey: string): void { + const txes = getTxes(target) + const tx: TxCreateDoc> = { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + objectSpace: core.space.Model, + objectId: extra._id ?? (propertyKey as Ref>), + objectClass: core.class.Attribute, + attributes: { + ...extra, + name: propertyKey, + index: getIndex(target, propertyKey), + type, + label, + attributeOf: txes._id, // undefined, need to fix later + ...getAttrs(target, propertyKey) + } + } + + clearAttrs(target, propertyKey) + + txes.txes.push(tx) + } +} + +/** + * @public + */ +export function Hidden () { + return function (target: any, propertyKey: string): void { + setAttr(target, propertyKey, 'hidden', true) + } +} + +/** + * @public + */ +export function ReadOnly () { + return function (target: any, propertyKey: string): void { + setAttr(target, propertyKey, 'readonly', true) + } +} + +/** + * @public + */ +export function Index (kind: IndexKind) { + return function (target: any, propertyKey: string): void { + setIndex(target, propertyKey, kind) + } +} + +/** + * @public + */ +export function Model ( + _class: Ref>, + _extends: Ref>, + domain?: Domain, + _implements?: Ref>[] +) { + return function classDecorator T> (constructor: C): void { + const txes = getTxes(constructor.prototype) + txes._id = _class + txes.extends = _class !== core.class.Obj ? _extends : undefined + txes.implements = _implements + txes.domain = domain + txes.kind = ClassifierKind.CLASS + } +} + +/** + * @public + */ +export function Implements (_interface: Ref>, _extends?: Ref>[]) { + return function classDecorator T> (constructor: C): void { + const txes = getTxes(constructor.prototype) + txes._id = _interface + txes.implements = _extends + txes.kind = ClassifierKind.INTERFACE + } +} + +/** + * @public + */ +export function Mixin (_class: Ref>, _extends: Ref>) { + return function classDecorator T> (constructor: C): void { + const txes = getTxes(constructor.prototype) + txes._id = _class + txes.extends = _extends + txes.kind = ClassifierKind.MIXIN + } +} + +/** + * @public + */ +export function UX ( + label: IntlString, + icon?: Asset, + shortLabel?: string, + sortingKey?: string, + filteringKey?: string, + pluralLabel?: IntlString +) { + return function classDecorator T> (constructor: C): void { + const txes = getTxes(constructor.prototype) + txes.label = label + txes.icon = icon + txes.shortLabel = shortLabel + txes.sortingKey = sortingKey + txes.filteringKey = filteringKey ?? sortingKey + txes.pluralLabel = pluralLabel + } +} + +function generateIds (objectId: Ref, txes: TxCreateDoc>[]): Tx[] { + return txes.map((tx) => { + const withId = { + ...tx, + // Do not override custom attribute id if specified + objectId: tx.objectId !== tx.attributes.name ? tx.objectId : `${objectId}_${tx.objectId}` + } + withId.attributes.attributeOf = objectId as Ref> + return withId + }) +} + +const txFactory = new TxFactory(core.account.System) + +function _generateTx (tx: ClassTxes): Tx[] { + const objectId = tx._id + const _cl = { + [ClassifierKind.CLASS]: core.class.Class, + [ClassifierKind.INTERFACE]: core.class.Interface, + [ClassifierKind.MIXIN]: core.class.Mixin + } + const createTx = txFactory.createTxCreateDoc( + _cl[tx.kind], + core.space.Model, + { + ...(tx.domain !== undefined ? { domain: tx.domain } : {}), + kind: tx.kind, + label: tx.label, + icon: tx.icon, + ...(tx.kind === ClassifierKind.INTERFACE + ? { extends: tx.implements } + : { extends: tx.extends, implements: tx.implements }), + ...(tx.kind === ClassifierKind.INTERFACE + ? { extends: tx.implements } + : { + shortLabel: tx.shortLabel, + sortingKey: tx.sortingKey, + filteringKey: tx.filteringKey, + pluralLabel: tx.pluralLabel + }) + }, + objectId + ) + return [createTx, ...generateIds(objectId, tx.txes as TxCreateDoc>[])] +} + +/** + * @public + */ +export class Builder { + private readonly txes: Tx[] = [] + readonly hierarchy = new Hierarchy() + + onTx?: (tx: Tx) => void + + createModel (...classes: Array Obj>): void { + const txes = classes.map((ctor) => getTxes(ctor.prototype)) + const byId = new Map() + + txes.forEach((tx) => { + byId.set(tx._id, tx) + }) + + Array.from(byId.entries()).forEach(([id, txes]) => { + if (txes.kind === ClassifierKind.CLASS && txes.domain !== undefined && txes.extends !== undefined) { + let parentTxes: ClassTxes | undefined = txes + let parentDomain: Domain | undefined + do { + parentTxes = parentTxes.extends === undefined ? undefined : byId.get(parentTxes.extends) + parentDomain = parentTxes === undefined ? undefined : parentTxes.domain + } while (parentTxes !== undefined && parentDomain === undefined) + if (parentDomain !== undefined) { + throw new Error( + `Class '${id}' should not specify its own domain '${txes.domain}', as it already extends class '${parentTxes?._id}' in domain '${parentDomain}'` + ) + } + } + }) + + const generated = this.generateTransactions(txes, byId) + + for (const tx of generated) { + this.txes.push(tx) + this.onTx?.(tx) + this.hierarchy.tx(tx) + } + } + + private generateTransactions (txes: ClassTxes[], byId: Map): Tx[] { + const graph = this.createGraph(txes) + const sorted = toposort(graph) + .reverse() + .map((edge) => byId.get(edge)) + return sorted.flatMap((tx) => (tx != null ? _generateTx(tx) : [])) + } + + private createGraph (txes: ClassTxes[]): [string, string | undefined][] { + return txes.map((tx) => [tx._id, tx.extends] as [string, string | undefined]) + } + + // do we need this? + createDoc( + _class: Ref>, + space: Ref, + attributes: Data, + objectId?: Ref, + modifiedBy?: PersonId + ): T { + const tx = txFactory.createTxCreateDoc(_class, space, attributes, objectId) + if (modifiedBy !== undefined) { + tx.modifiedBy = modifiedBy + } + this.txes.push(tx) + this.onTx?.(tx) + this.hierarchy.tx(tx) + return TxProcessor.createDoc2Doc(tx) + } + + mixin( + objectId: Ref, + objectClass: Ref>, + mixin: Ref>, + attributes: MixinData + ): void { + const tx = txFactory.createTxMixin(objectId, objectClass, core.space.Model, mixin, attributes) + this.txes.push(tx) + this.onTx?.(tx) + this.hierarchy.tx(tx) + } + + getTxes (): Tx[] { + return [...this.txes] + } +} + +// T Y P E S + +/** + * @public + */ +export function TypeString (): Type { + return { _class: core.class.TypeString, label: core.string.String, icon: core.icon.TypeString } +} + +/** + * @public + */ +export function TypeRelation (): Type { + return { _class: core.class.TypeRelation, label: core.string.Relation, icon: core.icon.TypeRef } +} + +/** + * @public + */ +export function TypeBlob (): Type { + return { _class: core.class.TypeBlob, label: core.string.String, icon: core.icon.TypeBlob } +} + +/** + * @public + */ +export function TypeHyperlink (): Type { + return { _class: core.class.TypeHyperlink, label: core.string.Hyperlink, icon: core.icon.TypeHyperlink } +} + +/** + * @public + */ +export function TypeNumber (min?: number, max?: number, digits?: number): TypeNumberType { + return { _class: core.class.TypeNumber, label: core.string.Number, icon: core.icon.TypeNumber, min, max, digits } +} + +/** + * @public + */ +export function TypeMarkup (): Type { + return { _class: core.class.TypeMarkup, label: core.string.Markup, icon: core.icon.TypeMarkup } +} + +/** + * @public + */ +export function TypeRecord (): Type> { + return { _class: core.class.TypeRecord, label: core.string.Record, icon: core.icon.TypeRecord } +} + +/** + * @public + */ +export function TypeIntlString (): Type { + return { _class: core.class.TypeIntlString, label: core.string.IntlString, icon: core.icon.TypeRef } +} + +/** + * @public + */ +export function TypeBoolean (): Type { + return { _class: core.class.TypeBoolean, label: core.string.Boolean, icon: core.icon.TypeBoolean } +} + +/** + * @public + */ +export function TypeTimestamp (): Type { + return { _class: core.class.TypeTimestamp, label: core.string.Timestamp, icon: core.icon.TypeDate } +} + +/** + * @public + */ +export function TypeDate (mode: DateRangeMode = DateRangeMode.DATE, withShift: boolean = true): TypeDateType { + return { _class: core.class.TypeDate, label: core.string.Date, icon: core.icon.TypeDate, mode, withShift } +} + +/** + * @public + */ +export function TypeRef (_class: Ref>): RefTo { + return { _class: core.class.RefTo, label: core.string.Ref, icon: core.icon.TypeRef, to: _class } +} + +/** + * @public + */ +export function TypeIdentifier (of: Ref): TypeIdentifierType { + return { _class: core.class.TypeIdentifier, label: core.string.Id, icon: core.icon.TypeRef, of } +} + +/** + * @public + */ +export function TypeEnum (of: Ref): EnumOf { + return { _class: core.class.EnumOf, label: core.string.Enum, icon: core.icon.TypeEnumOf, of } +} + +/** + * @public + */ +export function TypeFileSize (): Type { + return { _class: core.class.TypeFileSize, label: core.string.Size, icon: core.icon.TypeNumber } +} + +/** + * @public + */ +export function TypeAny ( + presenter: AnyComponent, + label: IntlString, + editor?: AnyComponent +): TypeAnyType { + return { _class: core.class.TypeAny, label, presenter, editor } +} + +/** + * @public + */ +export function Collection (clazz: Ref>, itemLabel?: IntlString): TypeCollection { + return { + _class: core.class.Collection, + label: core.string.Collection, + icon: core.icon.TypeCollection, + of: clazz, + itemLabel + } +} + +/** + * @public + */ +export function ArrOf> (type: Type): TypeArrOf { + return { _class: core.class.ArrOf, label: core.string.Array, of: type, icon: core.icon.TypeArray } +} + +/** + * @public + */ +export function TypeCollaborativeDoc (): Type { + return { _class: core.class.TypeCollaborativeDoc, label: core.string.MarkupBlobRef, icon: core.icon.TypeMarkup } +} + +/** + * @public + */ +export function TypeRank (): Type { + return { _class: core.class.TypeRank, label: core.string.Rank, icon: core.icon.TypeRank } +} + +export function TypePersonId (): Type { + return { _class: core.class.TypePersonId, label: core.string.PersonId } +} + +export function TypeAccountUuid (): Type { + return { _class: core.class.TypeAccountUuid, label: core.string.AccountId } +} diff --git a/foundations/core/packages/model/src/index.ts b/foundations/core/packages/model/src/index.ts new file mode 100644 index 0000000000..a6d143ca66 --- /dev/null +++ b/foundations/core/packages/model/src/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './dsl' +export * from './migration' +export * from './utils' diff --git a/foundations/core/packages/model/src/migration.ts b/foundations/core/packages/model/src/migration.ts new file mode 100644 index 0000000000..0c97d1ebb0 --- /dev/null +++ b/foundations/core/packages/model/src/migration.ts @@ -0,0 +1,312 @@ +import { type AccountClient } from '@hcengineering/account-client' +import { Analytics } from '@hcengineering/analytics' +import core, { + type Class, + type Client, + DOMAIN_MIGRATION, + DOMAIN_TX, + type Data, + type Doc, + type DocumentQuery, + type Domain, + type FindOptions, + type Hierarchy, + type MeasureContext, + type MigrationState, + type ModelDb, + type ObjQueryType, + type Rank, + type Ref, + SortingOrder, + type Space, + TxOperations, + type UnsetOptions, + type WorkspaceIds, + generateId +} from '@hcengineering/core' +import { makeRank } from '@hcengineering/rank' +import { type StorageAdapter } from '@hcengineering/storage' +import { type ModelLogger } from './utils' + +/** + * @public + */ +export type MigrateUpdate = Partial & UnsetOptions & Record + +/** + * @public + */ +export interface MigrationResult { + matched: number + updated: number +} + +/** + * @public + */ +export type MigrationDocumentQuery = { + [P in keyof T]?: ObjQueryType | null +} & { + $search?: string + // support nested queries e.g. 'user.friends.name' + // this will mark all unrecognized properties as any (including nested queries) + [key: string]: any +} + +/** + * @public + */ +export interface MigrationIterator { + next: (count: number) => Promise + close: () => Promise +} + +/** + * @public + * Client to perform model upgrades + */ +export interface MigrationClient { + // Raw collection operations + + // Raw FIND, allow to find documents inside domain. + find: ( + domain: Domain, + query: MigrationDocumentQuery, + options?: Omit, 'lookup'> + ) => Promise + + // Raw group by, allow to group documents inside domain. + groupBy: (domain: Domain, field: string, query?: DocumentQuery

) => Promise> + + // Traverse documents + traverse: ( + domain: Domain, + query: MigrationDocumentQuery, + options?: Pick, 'sort' | 'limit' | 'projection'> + ) => Promise> + + // Allow to raw update documents inside domain. + update: ( + domain: Domain, + query: MigrationDocumentQuery, + operations: MigrateUpdate + ) => Promise + + bulk: ( + domain: Domain, + operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] + ) => Promise + + // Move documents per domain + move: ( + sourceDomain: Domain, + query: DocumentQuery, + targetDomain: Domain, + size?: number + ) => Promise + + create: (domain: Domain, doc: T | T[]) => Promise + delete: (domain: Domain, _id: Ref) => Promise + deleteMany: (domain: Domain, query: DocumentQuery) => Promise + + hierarchy: Hierarchy + model: ModelDb + + migrateState: Map> + storageAdapter: StorageAdapter + accountClient: AccountClient + + wsIds: WorkspaceIds + + fullReindex: () => Promise + reindex: (domain: Domain, classes: Ref>[]) => Promise + readonly logger: ModelLogger + readonly ctx: MeasureContext +} + +/** + * @public + */ +export type MigrationUpgradeClient = Client +export type MigrateMode = 'create' | 'upgrade' + +/** + * @public + */ +export interface MigrateOperation { + // Perform low level migration prior to the model update + preMigrate?: (client: MigrationClient, logger: ModelLogger, mode: MigrateMode) => Promise + // Perform low level migration + migrate: (client: MigrationClient, mode: MigrateMode) => Promise + // Perform high level upgrade operations. + upgrade: ( + state: Map>, + client: () => Promise, + mode: MigrateMode + ) => Promise +} + +/** + * @public + */ +export interface Migrations { + state: string + mode?: MigrateMode // If set only applied to specified mode + func: (client: MigrationClient, mode: MigrateMode) => Promise +} + +/** + * @public + */ +export interface UpgradeOperations { + state: string + mode?: MigrateMode // If set only applied to specified mode + func: (client: MigrationUpgradeClient, mode: MigrateMode) => Promise +} + +/** + * @public + */ +export async function tryMigrate ( + mode: MigrateMode, + client: MigrationClient, + plugin: string, + migrations: Migrations[] +): Promise { + const states = client.migrateState.get(plugin) ?? new Set() + for (const migration of migrations) { + if (states.has(migration.state)) continue + if (migration.mode == null || migration.mode === mode) { + try { + client.logger.log('running migration', { plugin, state: migration.state }) + await migration.func(client, mode) + } catch (err: any) { + client.logger.error('Failed to run migration', { plugin, state: migration.state, err }) + Analytics.handleError(err) + continue + } + } + const st: MigrationState = { + plugin, + state: migration.state, + space: core.space.Configuration, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + _class: core.class.MigrationState, + _id: generateId() + } + await client.create(DOMAIN_MIGRATION, st) + } +} + +/** + * @public + */ +export async function tryUpgrade ( + mode: MigrateMode, + state: Map>, + client: () => Promise, + plugin: string, + migrations: UpgradeOperations[] +): Promise { + const states = state.get(plugin) ?? new Set() + for (const upgrades of migrations) { + if (states.has(upgrades.state)) continue + const _client = await client() + if (upgrades.mode == null || upgrades.mode === mode) { + try { + await upgrades.func(_client, mode) + } catch (err: any) { + console.error(err) + Analytics.handleError(err) + continue + } + } + const st: Data = { + plugin, + state: upgrades.state + } + const tx = new TxOperations(_client, core.account.System) + await tx.createDoc(core.class.MigrationState, core.space.Configuration, st) + } +} + +type DefaultSpaceData = Pick +type RequiredData = Omit, keyof DefaultSpaceData> & Partial> + +/** + * @public + */ +export async function createDefaultSpace ( + client: MigrationUpgradeClient, + _id: Ref, + props: RequiredData, + _class: Ref> = core.class.SystemSpace +): Promise { + const defaults: DefaultSpaceData = { + description: '', + private: false, + archived: false, + members: [] + } + const data: Data = { + ...defaults, + ...props + } + const tx = new TxOperations(client, core.account.System) + const current = await tx.findOne(core.class.Space, { + _id + }) + if (current === undefined || current._class !== _class) { + if (current !== undefined && current._class !== _class) { + await tx.remove(current) + } + await tx.createDoc(_class, core.space.Space, data, _id) + } +} + +/** + * @public + */ +export async function migrateSpace ( + client: MigrationClient, + from: Ref, + to: Ref, + domains: Domain[] +): Promise { + for (const domain of domains) { + await client.update(domain, { space: from }, { space: to }) + } + await client.update(DOMAIN_TX, { objectSpace: from }, { objectSpace: to }) +} + +export async function migrateSpaceRanks (client: MigrationClient, domain: Domain, space: Space): Promise { + type WithRank = Doc & { rank: Rank } + + const iterator = await client.traverse( + domain, + { space: space._id, rank: { $exists: true } }, + { sort: { rank: SortingOrder.Ascending } } + ) + + try { + let rank = '0|100000:' + + while (true) { + const docs = await iterator.next(1000) + if (docs === null || docs.length === 0) { + break + } + + const updates: { filter: MigrationDocumentQuery>, update: MigrateUpdate> }[] = [] + for (const doc of docs) { + rank = makeRank(rank, undefined) + updates.push({ filter: { _id: doc._id }, update: { rank } }) + } + + await client.bulk(domain, updates) + } + } finally { + await iterator.close() + } +} diff --git a/foundations/core/packages/model/src/utils.ts b/foundations/core/packages/model/src/utils.ts new file mode 100644 index 0000000000..e333ebbde3 --- /dev/null +++ b/foundations/core/packages/model/src/utils.ts @@ -0,0 +1,96 @@ +import { + type Class, + type Data, + type Doc, + type DocumentUpdate, + type Ref, + type Space, + type TxOperations, + type IdMap +} from '@hcengineering/core' +import { deepEqual } from 'fast-equals' + +function toUndef (value: any): any { + return value === null ? undefined : value +} + +function diffAttributes (doc: Data, newDoc: Data): DocumentUpdate { + const result: DocumentUpdate = {} + const allDocuments = new Map(Object.entries(doc)) + const newDocuments = new Map(Object.entries(newDoc)) + + for (const [key, value] of allDocuments) { + if (!newDocuments.has(key)) { + continue + } + + const newValue = toUndef(newDocuments.get(key)) + if (!deepEqual(newValue, toUndef(value))) { + // update is required, since values are different + result[key] = newValue + } + } + for (const [key, value] of newDocuments) { + const oldValue = toUndef(allDocuments.get(key)) + if (oldValue === undefined && value !== undefined) { + // Update with new value. + result[key] = value + } + } + return result +} + +/** + * Create or update document if modified only by system account. + * @public + */ +export async function createOrUpdate ( + client: TxOperations, + _class: Ref>, + space: Ref, + data: Data, + _id: Ref, + cache?: IdMap +): Promise { + const existingDoc = cache !== undefined ? cache.get(_id) : await client.findOne(_class, { _id }) + if (existingDoc !== undefined) { + const { _class: _oldClass, _id, space: _oldSpace, modifiedBy, modifiedOn, ...oldData } = existingDoc + if (modifiedBy === client.txFactory.account) { + const updateOp = diffAttributes(oldData, data) + if (Object.keys(updateOp).length > 0) { + await client.update(existingDoc, updateOp) + } + } + } else { + await client.createDoc(_class, space, data, _id) + } +} + +/** + * @public + */ +export interface ModelLogger { + log: (msg: string, data: any) => void + error: (msg: string, err: any) => void +} + +const errorPrinter = ({ message, stack, ...rest }: Error): object => ({ + message, + stack, + ...rest +}) +function replacer (value: any): any { + return value instanceof Error ? errorPrinter(value) : value +} + +/** + * @public + */ +export const consoleModelLogger: ModelLogger = { + log (msg: string, data: any): void { + console.log(msg, data) + }, + error (msg: string, data: any): void { + console.error(msg, replacer(data)) + } +} diff --git a/foundations/core/packages/model/tsconfig.json b/foundations/core/packages/model/tsconfig.json new file mode 100644 index 0000000000..c6a877cf6c --- /dev/null +++ b/foundations/core/packages/model/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/platform/.eslintrc.js b/foundations/core/packages/platform/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/platform/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/platform/.npmignore b/foundations/core/packages/platform/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/platform/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/platform/CHANGELOG.json b/foundations/core/packages/platform/CHANGELOG.json new file mode 100644 index 0000000000..5419f6f502 --- /dev/null +++ b/foundations/core/packages/platform/CHANGELOG.json @@ -0,0 +1,101 @@ +{ + "name": "@hcengineering/platform", + "entries": [ + { + "version": "0.7.18", + "tag": "@hcengineering/platform_v0.7.18", + "date": "Fri, 31 Oct 2025 20:11:13 GMT", + "comments": { + "patch": [ + { + "comment": "add pwd login locked status" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/platform_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/platform_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/platform_v0.6.5", + "date": "Sun, 08 Aug 2021 11:34:23 GMT", + "comments": { + "patch": [ + { + "comment": "ES6 target" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/platform_v0.6.4", + "date": "Sun, 08 Aug 2021 10:14:57 GMT", + "comments": { + "patch": [ + { + "comment": "Export status codes" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/platform_v0.6.3", + "date": "Wed, 04 Aug 2021 21:18:44 GMT", + "comments": { + "patch": [ + { + "comment": "fix" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/platform_v0.6.2", + "date": "Wed, 04 Aug 2021 20:48:46 GMT", + "comments": { + "patch": [ + { + "comment": "npmignore" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/platform_v0.6.1", + "date": "Wed, 04 Aug 2021 17:38:30 GMT", + "comments": { + "patch": [ + { + "comment": "Minor changes for publish" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/platform/CHANGELOG.md b/foundations/core/packages/platform/CHANGELOG.md new file mode 100644 index 0000000000..5e302bba35 --- /dev/null +++ b/foundations/core/packages/platform/CHANGELOG.md @@ -0,0 +1,60 @@ +# Change Log - @hcengineering/platform + +This log was last generated on Fri, 31 Oct 2025 20:11:13 GMT and should not be manually modified. + +## 0.7.18 +Fri, 31 Oct 2025 20:11:13 GMT + +### Patches + +- add pwd login locked status + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.0 +Sun, 08 Aug 2021 11:34:23 GMT + +### Patches + +- ES6 target + +## 0.7.0 +Sun, 08 Aug 2021 10:14:57 GMT + +### Patches + +- Export status codes + +## 0.7.0 +Wed, 04 Aug 2021 21:18:44 GMT + +### Patches + +- fix + +## 0.7.0 +Wed, 04 Aug 2021 20:48:46 GMT + +### Patches + +- npmignore + +## 0.7.0 +Wed, 04 Aug 2021 17:38:30 GMT + +### Patches + +- Minor changes for publish + diff --git a/foundations/core/packages/platform/config/rig.json b/foundations/core/packages/platform/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/platform/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/platform/jest.config.js b/foundations/core/packages/platform/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/platform/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/platform/lang/cs.json b/foundations/core/packages/platform/lang/cs.json new file mode 100644 index 0000000000..aa90e18d92 --- /dev/null +++ b/foundations/core/packages/platform/lang/cs.json @@ -0,0 +1,31 @@ +{ + "status": { + "LoadingPlugin": "Načítání pluginu {plugin}...", + "UnknownError": "Neznámá chyba: {message}", + "InvalidId": "Neplatné ID: {id}", + "BadRequest": "Špatný požadavek", + "Forbidden": "Zakázáno", + "Conflict": "Konflikt", + "ExpiredLink": "Tento odkaz na pozvánku vypršel", + "Unauthorized": "Neoprávněný přístup", + "UnknownMethod": "Neznámá metoda: {method}", + "InternalServerError": "Interní chyba serveru", + "MaintenanceWarning": "Plánovaná údržba za", + "MaintenanceWarningTime": "{time, plural, =1 {méně než minutu} other {# minut}}", + "AccountNotFound": "Účet nenalezen", + "AccountNotConfirmed": "Účet není potvrzen", + "WorkspaceNotFound": "Pracovní prostor nenalezen", + "InvalidPassword": "Neplatné heslo", + "AccountAlreadyExists": "Účet již existuje", + "WorkspaceRateLimit": "Server je zaneprázdněný. Prosím, chvíli počkejte a zkuste to znovu", + "AccountAlreadyConfirmed": "Účet již byl potvrzen", + "WorkspaceAlreadyExists": "Pracovní prostor již existuje", + "InvalidOtp": "Neplatný kód", + "PasswordLoginLocked": "Přihlášení heslem je zablokováno z důvodu příliš mnoha neúspěšných pokusů. Pomocí metody přihlášení OTP odemkněte svůj účet.", + "InviteNotFound": "Pozvánka s e-mailem:{email} nenalezena.", + "WorkspaceLimitReached": "Dosáhli jste limitu pracovních prostorů. Kontaktujte nás...", + "ReadOnlyAccount": "Anonymní hostovská ukázka", + "SystemAccount": "Systémový účet", + "SocialIdAlreadyExists": "Sociální ID již existuje" + } +} diff --git a/foundations/core/packages/platform/lang/de.json b/foundations/core/packages/platform/lang/de.json new file mode 100644 index 0000000000..2e70d7586b --- /dev/null +++ b/foundations/core/packages/platform/lang/de.json @@ -0,0 +1,31 @@ +{ + "status": { + "LoadingPlugin": "Plugin {plugin} wird geladen...", + "UnknownError": "Unbekannter Fehler: {message}", + "InvalidId": "Ungültige ID: {id}", + "BadRequest": "Fehlerhafte Anfrage", + "Forbidden": "Zugriff verweigert", + "Conflict": "Konflikt", + "ExpiredLink": "Dieser Einladungslink ist abgelaufen", + "Unauthorized": "Nicht autorisiert", + "UnknownMethod": "Unbekannte Methode: {method}", + "InternalServerError": "Interner Serverfehler", + "MaintenanceWarning": "Wartung geplant in", + "MaintenanceWarningTime": "{time, plural, =1 {weniger als einer Minute} other {# Minuten}}", + "AccountNotFound": "Konto nicht gefunden", + "AccountNotConfirmed": "Konto nicht bestätigt", + "WorkspaceNotFound": "Arbeitsbereich nicht gefunden", + "InvalidPassword": "Ungültiges Passwort", + "AccountAlreadyExists": "Konto existiert bereits", + "WorkspaceRateLimit": "Server ist ausgelastet, bitte warten Sie einen Moment und versuchen Sie es erneut", + "AccountAlreadyConfirmed": "Konto wurde bereits bestätigt", + "WorkspaceAlreadyExists": "Arbeitsbereich existiert bereits", + "InvalidOtp": "Ungültiger Code", + "PasswordLoginLocked": "Die Passwort-Anmeldung ist aufgrund zu vieler fehlgeschlagener Versuche gesperrt. Bitte verwenden Sie eine OTP-Anmeldungsmethode, um Ihr Konto freizuschalten.", + "InviteNotFound": "Einladung mit E-Mail:{email} nicht gefunden.", + "WorkspaceLimitReached": "Sie haben das Arbeitsbereichslimit erreicht. Bitte kontaktieren Sie uns...", + "ReadOnlyAccount": "Anonymer Gast-Demo", + "SystemAccount": "Systemkonto", + "SocialIdAlreadyExists": "Social ID existiert bereits" + } +} diff --git a/foundations/core/packages/platform/lang/en.json b/foundations/core/packages/platform/lang/en.json new file mode 100644 index 0000000000..80a3469ab4 --- /dev/null +++ b/foundations/core/packages/platform/lang/en.json @@ -0,0 +1,30 @@ +{ + "status": { + "LoadingPlugin": "Loading plugin {plugin}...", + "UnknownError": "Unknown error: {message}", + "InvalidId": "Invalid Id: {id}", + "BadRequest": "Bad request", + "Forbidden": "Forbidden", + "Conflict": "Conflict", + "ExpiredLink": "This invite link is expired", + "Unauthorized": "Unauthorized", + "UnknownMethod": "Unknown method: {method}", + "InternalServerError": "Internal server error", + "MaintenanceWarning": "Maintenance Scheduled in", + "MaintenanceWarningTime": "{time, plural, =1 {less than a minute} other {# minutes}}", + "AccountNotFound": "Account not found or the provided credentials are incorrect", + "AccountNotConfirmed": "Account not confirmed", + "WorkspaceNotFound": "Workspace not found", + "AccountAlreadyExists": "Account already exists", + "WorkspaceRateLimit": "Server is busy, Please wait a bit and try again", + "AccountAlreadyConfirmed": "Account already confirmed", + "WorkspaceAlreadyExists": "Workspace already exists", + "InvalidOtp": "Invalid code", + "PasswordLoginLocked": "Password login is locked due to too many failed attempts. Please use an OTP login method to unlock your account.", + "InviteNotFound": "Invitation with email:{email} not found.", + "WorkspaceLimitReached": "You have reached the workspace limit. Please contact us...", + "ReadOnlyAccount": "Anonymous Guest Demo", + "SystemAccount": "System account", + "SocialIdAlreadyExists": "Social ID already exists" + } +} diff --git a/foundations/core/packages/platform/lang/es.json b/foundations/core/packages/platform/lang/es.json new file mode 100644 index 0000000000..f1aa67ba95 --- /dev/null +++ b/foundations/core/packages/platform/lang/es.json @@ -0,0 +1,31 @@ +{ + "status": { + "LoadingPlugin": "Cargando complemento {plugin}...", + "UnknownError": "Error desconocido: {message}", + "InvalidId": "Id no válido: {id}", + "BadRequest": "Solicitud incorrecta", + "Forbidden": "Prohibido", + "Conflict": "Conflicto", + "ExpiredLink": "Este enlace de invitación ha caducado", + "Unauthorized": "No autorizado", + "UnknownMethod": "Método desconocido: {method}", + "InternalServerError": "Error interno del servidor", + "MaintenanceWarning": "Mantenimiento programado dentro de", + "MaintenanceWarningTime": "{time, plural, =1 {menos de un minuto} other {# minutos}}", + "AccountNotFound": "Cuenta no encontrada", + "AccountNotConfirmed": "Cuenta no confirmada", + "WorkspaceNotFound": "Espacio de trabajo no encontrado", + "InvalidPassword": "Contraseña inválida", + "AccountAlreadyExists": "La cuenta ya existe", + "WorkspaceRateLimit": "El servidor está ocupado. Espere un momento e inténtelo de nuevo", + "AccountAlreadyConfirmed": "La cuenta ya está confirmada", + "WorkspaceAlreadyExists": "El espacio de trabajo ya existe", + "InvalidOtp": "Código inválido", + "PasswordLoginLocked": "El inicio de sesión con contraseña está bloqueado debido a demasiados intentos fallidos. Use el método de inicio de sesión OTP para desbloquear su cuenta.", + "InviteNotFound": "Invitación con correo electrónico:{email} no encontrada.", + "WorkspaceLimitReached": "Ha alcanzado el límite de espacios de trabajo. Póngase en contacto con nosotros...", + "ReadOnlyAccount": "Demo de invitado anónimo", + "SystemAccount": "Cuenta del sistema", + "SocialIdAlreadyExists": "El ID social ya existe" + } +} diff --git a/foundations/core/packages/platform/lang/fr.json b/foundations/core/packages/platform/lang/fr.json new file mode 100644 index 0000000000..cae2833fc1 --- /dev/null +++ b/foundations/core/packages/platform/lang/fr.json @@ -0,0 +1,31 @@ +{ + "status": { + "LoadingPlugin": "Chargement du plugin {plugin}...", + "UnknownError": "Erreur inconnue : {message}", + "InvalidId": "Id invalide : {id}", + "BadRequest": "Mauvaise requête", + "Forbidden": "Interdit", + "Conflict": "Conflit", + "ExpiredLink": "Ce lien d'invitation est expiré", + "Unauthorized": "Non autorisé", + "UnknownMethod": "Méthode inconnue : {method}", + "InternalServerError": "Erreur interne du serveur", + "MaintenanceWarning": "Maintenance prévue dans", + "MaintenanceWarningTime": "{time, plural, =1 {moins d'une minute} other {# minutes}}", + "AccountNotFound": "Compte non trouvé", + "AccountNotConfirmed": "Compte non confirmé", + "WorkspaceNotFound": "Espace de travail non trouvé", + "InvalidPassword": "Mot de passe invalide", + "AccountAlreadyExists": "Le compte existe déjà", + "WorkspaceRateLimit": "Le serveur est occupé, veuillez patienter un moment et réessayer", + "AccountAlreadyConfirmed": "Compte déjà confirmé", + "WorkspaceAlreadyExists": "L'espace de travail existe déjà", + "InvalidOtp": "Code invalide", + "PasswordLoginLocked": "La connexion par mot de passe est verrouillée en raison de trop nombreuses tentatives échouées. Veuillez utiliser une méthode de connexion OTP pour déverrouiller votre compte.", + "InviteNotFound": "Invitation avec e-mail:{email} introuvable.", + "WorkspaceLimitReached": "Vous avez atteint la limite d'espace de travail. Veuillez contacter nous...", + "ReadOnlyAccount": "Démo invité anonyme", + "SystemAccount": "Compte système", + "SocialIdAlreadyExists": "L'ID social existe déjà" + } +} diff --git a/foundations/core/packages/platform/lang/it.json b/foundations/core/packages/platform/lang/it.json new file mode 100644 index 0000000000..a8577635e2 --- /dev/null +++ b/foundations/core/packages/platform/lang/it.json @@ -0,0 +1,31 @@ +{ + "status": { + "LoadingPlugin": "Caricamento del plugin {plugin}...", + "UnknownError": "Errore sconosciuto: {message}", + "InvalidId": "Id non valido: {id}", + "BadRequest": "Richiesta non valida", + "Forbidden": "Proibito", + "Conflict": "Conflitto", + "ExpiredLink": "Questo link di invito è scaduto", + "Unauthorized": "Non autorizzato", + "UnknownMethod": "Metodo sconosciuto: {method}", + "InternalServerError": "Errore interno del server", + "MaintenanceWarning": "Manutenzione programmata tra", + "MaintenanceWarningTime": "{time, plural, =1 {meno di un minuto} other {# minuti}}", + "AccountNotFound": "Account non trovato", + "AccountNotConfirmed": "Account non confermato", + "WorkspaceNotFound": "Spazio di lavoro non trovato", + "InvalidPassword": "Password non valida", + "AccountAlreadyExists": "Account già esistente", + "WorkspaceRateLimit": "Il server è occupato, attendere un momento e riprovare", + "AccountAlreadyConfirmed": "Account già confermato", + "WorkspaceAlreadyExists": "Spazio di lavoro già esistente", + "InvalidOtp": "Codice non valido", + "PasswordLoginLocked": "L'accesso con password è bloccato a causa di troppi tentativi non riusciti. Utilizza un metodo di accesso OTP per sbloccare il tuo account.", + "InviteNotFound": "Invito con email:{email} non trovato.", + "WorkspaceLimitReached": "Hai raggiunto il limite di spazi di lavoro. Contattaci...", + "ReadOnlyAccount": "Demo ospite anonimo", + "SystemAccount": "Account di sistema", + "SocialIdAlreadyExists": "L'ID social esiste già" + } +} diff --git a/foundations/core/packages/platform/lang/ja.json b/foundations/core/packages/platform/lang/ja.json new file mode 100644 index 0000000000..8a54330f22 --- /dev/null +++ b/foundations/core/packages/platform/lang/ja.json @@ -0,0 +1,30 @@ +{ + "status": { + "LoadingPlugin": "プラグイン {plugin} を読み込み中…", + "UnknownError": "不明なエラー: {message}", + "InvalidId": "無効なID: {id}", + "BadRequest": "不正なリクエスト", + "Forbidden": "アクセスが禁止されています", + "Conflict": "競合", + "ExpiredLink": "この招待リンクは期限切れです", + "Unauthorized": "認証されていません", + "UnknownMethod": "不明なメソッド: {method}", + "InternalServerError": "サーバー内部エラー", + "MaintenanceWarning": "メンテナンスが予定されています", + "MaintenanceWarningTime": "{time, plural, =1 {1分以内に} other {#分後に}}", + "AccountNotFound": "アカウントが見つからないか、認証情報が間違っています", + "AccountNotConfirmed": "アカウントが確認されていません", + "WorkspaceNotFound": "ワークスペースが見つかりません", + "AccountAlreadyExists": "このアカウントはすでに存在します", + "WorkspaceRateLimit": "サーバーが混雑しています。しばらく待ってから再度お試しください", + "AccountAlreadyConfirmed": "このアカウントはすでに確認済みです", + "WorkspaceAlreadyExists": "ワークスペースはすでに存在します", + "InvalidOtp": "無効なコード", + "PasswordLoginLocked": "ログイン試行の失敗が多すぎるため、パスワードログインはロックされています。OTPログイン方法を使用してアカウントのロックを解除してください。", + "InviteNotFound": "メール:{email} の招待が見つかりません。", + "WorkspaceLimitReached": "作成可能なワークスペースの上限に達しました。お問い合わせください", + "ReadOnlyAccount": "匿名ゲストデモ", + "SystemAccount": "システムアカウント", + "SocialIdAlreadyExists": "ソーシャルIDは既に存在します" + } +} diff --git a/foundations/core/packages/platform/lang/pt.json b/foundations/core/packages/platform/lang/pt.json new file mode 100644 index 0000000000..a66bf1fcc7 --- /dev/null +++ b/foundations/core/packages/platform/lang/pt.json @@ -0,0 +1,31 @@ +{ + "status": { + "LoadingPlugin": "Carregando plugin {plugin}...", + "UnknownError": "Erro desconhecido: {message}", + "InvalidId": "Id inválido: {id}", + "BadRequest": "Pedido inválido", + "Forbidden": "Proibido", + "Conflict": "Conflito", + "ExpiredLink": "Este link de convite expirou", + "Unauthorized": "Não autorizado", + "UnknownMethod": "Método desconhecido: {method}", + "InternalServerError": "Erro interno do servidor", + "MaintenanceWarning": "Manutenção programada dentro de", + "MaintenanceWarningTime": "{time, plural, =1 {menos de um minuto} other {# minutos}}", + "AccountNotFound": "Conta não encontrada", + "AccountNotConfirmed": "Conta não confirmada", + "WorkspaceNotFound": "Espaço de trabalho não encontrado", + "InvalidPassword": "Senha inválida", + "AccountAlreadyExists": "Conta já existe", + "WorkspaceRateLimit": "O servidor está ocupado. Por favor, espere um pouco e tente novamente", + "AccountAlreadyConfirmed": "Conta já confirmada", + "WorkspaceAlreadyExists": "Espaço de trabalho já existe", + "InvalidOtp": "Código inválido", + "PasswordLoginLocked": "O login com senha está bloqueado devido a muitas tentativas falhadas. Use um método de login OTP para desbloquear sua conta.", + "InviteNotFound": "Convite com e-mail:{email} não encontrado.", + "WorkspaceLimitReached": "Você atingiu o limite de espaço de trabalho. Entre em contato conosco...", + "ReadOnlyAccount": "Demonstração anônima de convidado", + "SystemAccount": "Conta do sistema", + "SocialIdAlreadyExists": "ID social já existe" + } +} diff --git a/foundations/core/packages/platform/lang/ru.json b/foundations/core/packages/platform/lang/ru.json new file mode 100644 index 0000000000..8715a2ba63 --- /dev/null +++ b/foundations/core/packages/platform/lang/ru.json @@ -0,0 +1,31 @@ +{ + "status": { + "LoadingPlugin": "Загрузка плагина {plugin}...", + "UnknownError": "Неизвестная ошибка: {message}", + "InvalidId": "Некорректный Id: {id}", + "BadRequest": "Некорректный запрос", + "Forbidden": "Запрещено", + "Conflict": "Конфликт", + "ExpiredLink": "Ссылка истекла", + "Unauthorized": "Неавторизован", + "UnknownMethod": "Неизвестный метод: {method}", + "InternalServerError": "Внутренняя ошибка сервера", + "MaintenanceWarning": "Серверные работы запланированы через", + "MaintenanceWarningTime": "{time, plural, one {# минуту} few {# минуты} other {# минут}}", + "AccountNotFound": "Аккаунт не найден", + "AccountNotConfirmed": "Аккаунт не подтвержден", + "WorkspaceNotFound": "Рабочее пространство не найдено", + "InvalidPassword": "Неверный пароль", + "AccountAlreadyExists": "Аккаунт уже существует", + "WorkspaceRateLimit": "Сервер перегружен, пожалуйста, подождите", + "AccountAlreadyConfirmed": "Аккаунт уже подтвержден", + "WorkspaceAlreadyExists": "Рабочее пространство уже существует", + "InvalidOtp": "Неверный код", + "PasswordLoginLocked": "Вход по паролю заблокирован из-за слишком большого количества неудачных попыток. Используйте метод входа OTP для разблокировки аккаунта.", + "InviteNotFound": "Приглашение с электронной почтой:{email} не найдено.", + "WorkspaceLimitReached": "Вы достигли лимита рабочих пространств. Свяжитесь с нами...", + "ReadOnlyAccount": "Анонимное гостевое демо", + "SystemAccount": "Системный аккаунт", + "SocialIdAlreadyExists": "Социальный ID уже существует" + } +} diff --git a/foundations/core/packages/platform/lang/tr.json b/foundations/core/packages/platform/lang/tr.json new file mode 100644 index 0000000000..681b17fb03 --- /dev/null +++ b/foundations/core/packages/platform/lang/tr.json @@ -0,0 +1,30 @@ +{ + "status": { + "LoadingPlugin": "{plugin} eklentisi yükleniyor...", + "UnknownError": "Bilinmeyen hata: {message}", + "InvalidId": "Geçersiz Id: {id}", + "BadRequest": "Hatalı istek", + "Forbidden": "Yasak", + "Conflict": "Çakışma", + "ExpiredLink": "Bu davet bağlantısının süresi dolmuş", + "Unauthorized": "Yetkisiz", + "UnknownMethod": "Bilinmeyen metod: {method}", + "InternalServerError": "Dahili sunucu hatası", + "MaintenanceWarning": "Bakım Planlandı:", + "MaintenanceWarningTime": "{time, plural, =1 {bir dakikadan az} other {# dakika}}", + "AccountNotFound": "Hesap bulunamadı veya girilen kimlik bilgileri yanlış", + "AccountNotConfirmed": "Hesap doğrulanmadı", + "WorkspaceNotFound": "Çalışma alanı bulunamadı", + "AccountAlreadyExists": "Hesap zaten mevcut", + "WorkspaceRateLimit": "Sunucu meşgul, Lütfen biraz bekleyin ve tekrar deneyin", + "AccountAlreadyConfirmed": "Hesap zaten doğrulandı", + "WorkspaceAlreadyExists": "Çalışma alanı zaten mevcut", + "InvalidOtp": "Geçersiz kod", + "PasswordLoginLocked": "Çok fazla başarısız deneme nedeniyle parola girişi kilitlendi. Hesabınızın kilidini açmak için OTP giriş yöntemini kullanın.", + "InviteNotFound": "E-mail:{email} ile ilgili davet bulunamadı.", + "WorkspaceLimitReached": "Çalışma alanı limitine ulaştınız. Lütfen bizimle iletişime geçin...", + "ReadOnlyAccount": "Anonim Misafir Demo", + "SystemAccount": "Sistem hesabı", + "SocialIdAlreadyExists": "Sosyal ID zaten mevcut" + } +} diff --git a/foundations/core/packages/platform/lang/zh.json b/foundations/core/packages/platform/lang/zh.json new file mode 100644 index 0000000000..034b0824e5 --- /dev/null +++ b/foundations/core/packages/platform/lang/zh.json @@ -0,0 +1,30 @@ +{ + "status": { + "LoadingPlugin": "正在加载插件 {plugin}...", + "UnknownError": "未知错误: {message}", + "InvalidId": "无效的 Id: {id}", + "BadRequest": "错误的请求", + "Forbidden": "禁止访问", + "Conflict": "冲突", + "ExpiredLink": "此邀请链接已过期", + "Unauthorized": "未授权", + "UnknownMethod": "未知方法: {method}", + "InternalServerError": "内部服务器错误", + "MaintenanceWarning": "计划在 内进行维护 ", + "MaintenanceWarningTime": "{time, plural, =1 {不到一分钟} other {# 分钟}}", + "AccountNotFound": "账户未找到", + "AccountNotConfirmed": "账户未确认", + "WorkspaceNotFound": "工作区未找到", + "InvalidPassword": "无效的密码", + "AccountAlreadyExists": "账户已存在", + "WorkspaceRateLimit": "服务器繁忙,请稍后再试", + "AccountAlreadyConfirmed": "账户已确认", + "WorkspaceAlreadyExists": "工作区已存在", + "InvalidOtp": "无效的代码", + "PasswordLoginLocked": "由于登录尝试失败次数过多,密码登录已被锁定。请使用 OTP 登录方式解锁您的账户。", + "InviteNotFound": "未找到包含以下电子邮件的邀请:{email}。", + "WorkspaceLimitReached": "您已达到工作区限制。请联系我们...", + "ReadOnlyAccount": "匿名访客演示", + "SocialIdAlreadyExists": "社交ID已存在" + } +} diff --git a/foundations/core/packages/platform/package.json b/foundations/core/packages/platform/package.json new file mode 100644 index 0000000000..841cc6d919 --- /dev/null +++ b/foundations/core/packages/platform/package.json @@ -0,0 +1,65 @@ +{ + "name": "@hcengineering/platform", + "version": "0.7.18", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json", + "lang/**/*" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@types/jest": "^29.5.5", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "prettier": "^3.6.2", + "ts-jest": "^29.1.1", + "typescript": "^5.9.3", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "intl-messageformat": "^10.7.14" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + }, + "./lang/*.json": { + "require": "./lang/*.json", + "import": "./lang/*.json" + }, + "./lang": { + "require": "./lang", + "import": "./lang" + } + } +} diff --git a/foundations/core/packages/platform/src/__tests__/i18n.test.ts b/foundations/core/packages/platform/src/__tests__/i18n.test.ts new file mode 100644 index 0000000000..53211fe106 --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/i18n.test.ts @@ -0,0 +1,115 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Plugin, IntlString } from '../platform' +import platform, { plugin } from '../platform' +import { Severity, Status } from '../status' + +import { addStringsLoader, translate } from '../i18n' +import { addEventListener, PlatformEvent, removeEventListener } from '../event' + +const testId = 'test-strings' as Plugin + +const test = plugin(testId, { + string: { + loadingPlugin: '' as IntlString<{ plugin: string }> + } +}) + +describe('i18n', () => { + it('should translate string', async () => { + addStringsLoader(testId, async (locale: string) => await import(`./lang/${locale}.json`)) + const translated = await translate(test.string.loadingPlugin, { plugin: 'xxx' }) + expect(translated).toBe('Loading plugin xxx...') + }) + + it('should return id when no translation found', async () => { + const id = (testId + '.inexistent') as IntlString + const inexistent = await translate(id, {}) + expect(inexistent).toBe(id) + }) + + it('should cache translated string', async () => { + const translated = await translate(test.string.loadingPlugin, { plugin: 'xxx' }) + expect(translated).toBe('Loading plugin xxx...') + }) + + it('should emit status and return id when no loader', async () => { + expect.assertions(2) + const plugin = 'plugin-without-string-loader' + const message = `${plugin}:string:id` + + const checkStatus = new Status(Severity.ERROR, platform.status.NoLoaderForStrings, { plugin }) + const eventListener = async (event: string, data: any): Promise => { + expect(data).toEqual(checkStatus) + } + addEventListener(PlatformEvent, eventListener) + const translated = await translate(message as IntlString, {}) + expect(translated).toBe(message) + removeEventListener(PlatformEvent, eventListener) + }) + + it('should emit status and return id when bad loader', async () => { + expect.assertions(2) + const plugin = 'component-for-bad-loader' + const message = `${plugin}:string:id` + const errorMessage = 'bad loader' + addStringsLoader(plugin as Plugin, (locale: string) => { + throw new Error(errorMessage) + }) + + const checkStatus = new Status(Severity.ERROR, platform.status.UnknownError, { message: errorMessage }) + const eventListener = async (event: string, data: any): Promise => { + expect(data).toEqual(checkStatus) + } + addEventListener(PlatformEvent, eventListener) + const translated = await translate(message as IntlString, {}) + expect(translated).toBe(message) + removeEventListener(PlatformEvent, eventListener) + }) + + it('should cache error', async () => { + const plugin = 'component' + const message = `${plugin}:string:id` + + const checkStatus = new Status(Severity.ERROR, platform.status.NoLoaderForStrings, { plugin }) + let calls = 0 + const eventListener = async (event: string, data: any): Promise => { + ++calls + expect(data).toEqual(checkStatus) + } + + addEventListener(PlatformEvent, eventListener) + const t1 = await translate(message as IntlString, {}) + const t2 = await translate(message as IntlString, {}) + expect(t1).toBe(t2) + removeEventListener(PlatformEvent, eventListener) + expect(calls).toBe(1) + }) + + it('should return message when bad id', async () => { + expect.assertions(2) + const message = 'testMessage' as IntlString + const checkStatus = new Status(Severity.ERROR, platform.status.InvalidId, { id: message }) + const eventListener = async (event: string, data: any): Promise => { + expect(data).toEqual(checkStatus) + } + addEventListener(PlatformEvent, eventListener) + const translated = await translate(message, {}) + expect(translated).toBe(message) + removeEventListener(PlatformEvent, eventListener) + }) +}) diff --git a/foundations/core/packages/platform/src/__tests__/ident.test.ts b/foundations/core/packages/platform/src/__tests__/ident.test.ts new file mode 100644 index 0000000000..784294b313 --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/ident.test.ts @@ -0,0 +1,77 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { StatusCode, Plugin, Id } from '../platform' +import { plugin, mergeIds } from '../platform' +import { _parseId } from '../ident' + +describe('ident', () => { + const test = 'test' as Plugin + + it('should identify resources', () => { + const ids = plugin(test, { + status: { + MyString: '' as StatusCode + } + }) + expect(ids.status.MyString).toBe('test:status:MyString') + }) + + it('should merge ids', () => { + const ids = plugin(test, { + resource: { + MyString: '' as StatusCode + } + }) + const merged = mergeIds(test, ids, { + resource: { + OneMore: '' as StatusCode + }, + more: { + X: '' as StatusCode + } + }) + expect(merged.resource.MyString).toBe('test:resource:MyString') + expect(merged.resource.OneMore).toBe('test:resource:OneMore') + expect(merged.more.X).toBe('test:more:X') + }) + + it('should fail overwriting ids', () => { + const ids = plugin(test, { + resource: { + MyString: '' as StatusCode + } + }) + const f = (): any => + mergeIds(test, ids, { + resource: { + MyString: 'xxx' as StatusCode + } + }) + expect(f).toThrowError("'identify' overwrites") + }) + + it('should fail to parse id', () => { + expect(() => _parseId('bad id' as Id)).toThrowError('ERROR: platform:status:InvalidId') + }) + + it('should parse id', () => { + expect(_parseId('comp:res:X' as Id)).toEqual({ + kind: 'res', + component: 'comp', + name: 'X' + }) + }) +}) diff --git a/foundations/core/packages/platform/src/__tests__/lang/de.json b/foundations/core/packages/platform/src/__tests__/lang/de.json new file mode 100644 index 0000000000..34b6b8a228 --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/lang/de.json @@ -0,0 +1,5 @@ +{ + "string": { + "loadingPlugin": "Plugin ''{plugin}'' wird geladen..." + } +} \ No newline at end of file diff --git a/foundations/core/packages/platform/src/__tests__/lang/en.json b/foundations/core/packages/platform/src/__tests__/lang/en.json new file mode 100644 index 0000000000..e041a73fa6 --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/lang/en.json @@ -0,0 +1,5 @@ +{ + "string": { + "loadingPlugin": "Loading plugin ''{plugin}''..." + } +} diff --git a/foundations/core/packages/platform/src/__tests__/lang/es.json b/foundations/core/packages/platform/src/__tests__/lang/es.json new file mode 100644 index 0000000000..ebd344cec5 --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/lang/es.json @@ -0,0 +1,5 @@ +{ + "string": { + "loadingPlugin": "Cargando plugin ''{plugin}''..." + } +} \ No newline at end of file diff --git a/foundations/core/packages/platform/src/__tests__/lang/fr.json b/foundations/core/packages/platform/src/__tests__/lang/fr.json new file mode 100644 index 0000000000..c53944f623 --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/lang/fr.json @@ -0,0 +1,5 @@ +{ + "string": { + "loadingPlugin": "Chargement du plugin ''{plugin}''..." + } +} \ No newline at end of file diff --git a/foundations/core/packages/platform/src/__tests__/lang/it.json b/foundations/core/packages/platform/src/__tests__/lang/it.json new file mode 100644 index 0000000000..c037596d8d --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/lang/it.json @@ -0,0 +1,5 @@ +{ + "string": { + "loadingPlugin": "Caricamento del plugin ''{plugin}''..." + } +} diff --git a/foundations/core/packages/platform/src/__tests__/lang/ja.json b/foundations/core/packages/platform/src/__tests__/lang/ja.json new file mode 100644 index 0000000000..703e186d39 --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/lang/ja.json @@ -0,0 +1,5 @@ +{ + "string": { + "loadingPlugin": "プラグイン ''{plugin}'' を読み込み中…" + } +} \ No newline at end of file diff --git a/foundations/core/packages/platform/src/__tests__/lang/pt.json b/foundations/core/packages/platform/src/__tests__/lang/pt.json new file mode 100644 index 0000000000..9e57429683 --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/lang/pt.json @@ -0,0 +1,5 @@ +{ + "string": { + "loadingPlugin": "A Carregar plugin ''{plugin}''..." + } +} \ No newline at end of file diff --git a/foundations/core/packages/platform/src/__tests__/lang/zh.json b/foundations/core/packages/platform/src/__tests__/lang/zh.json new file mode 100644 index 0000000000..bbd2593c6b --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/lang/zh.json @@ -0,0 +1,5 @@ +{ + "string": { + "loadingPlugin": "正在加载插件 ''{plugin}''..." + } +} diff --git a/foundations/core/packages/platform/src/__tests__/plugin.ts b/foundations/core/packages/platform/src/__tests__/plugin.ts new file mode 100644 index 0000000000..95961aafe6 --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/plugin.ts @@ -0,0 +1,21 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export default async () => ({ + test: { + X: 'Test' + } +}) diff --git a/foundations/core/packages/platform/src/__tests__/resource.test.ts b/foundations/core/packages/platform/src/__tests__/resource.test.ts new file mode 100644 index 0000000000..a5b0ad0742 --- /dev/null +++ b/foundations/core/packages/platform/src/__tests__/resource.test.ts @@ -0,0 +1,41 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Plugin, IntlString, Resource } from '../platform' +import { plugin } from '../platform' +import { addLocation, getResource } from '../resource' + +describe('resource', () => { + const test = 'test' as Plugin + + const testPlugin = plugin(test, { + string: { + Hello: '' as IntlString<{ name: string }> + }, + test: { + X: '' as Resource + } + }) + + addLocation(test, async () => await import('./plugin')) + + it('should load resource', async () => { + const string = await getResource(testPlugin.test.X) + expect(string).toBe('Test') + const cached = await getResource(testPlugin.test.X) + expect(cached).toBe('Test') + }) +}) diff --git a/foundations/core/packages/platform/src/event.ts b/foundations/core/packages/platform/src/event.ts new file mode 100644 index 0000000000..138dbe6087 --- /dev/null +++ b/foundations/core/packages/platform/src/event.ts @@ -0,0 +1,99 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Status, OK, unknownError, Severity } from './status' + +/** + * @public + */ +export const PlatformEvent = 'platform-event' + +/** + * @public + */ +export type EventListener = (event: string, data: any) => Promise + +const eventListeners = new Map() + +/** + * @public + * @param event - + * @param listener - + */ +export function addEventListener (event: string, listener: EventListener): void { + const listeners = eventListeners.get(event) + if (listeners !== undefined) { + listeners.push(listener) + } else { + eventListeners.set(event, [listener]) + } +} + +/** + * @public + * @param event - + * @param listener - + */ +export function removeEventListener (event: string, listener: EventListener): void { + const listeners = eventListeners.get(event) + if (listeners !== undefined) { + listeners.splice(listeners.indexOf(listener), 1) + } +} + +/** + * @public + */ +export async function broadcastEvent (event: string, data: any): Promise { + const listeners = eventListeners.get(event) + if (listeners !== undefined) { + const promises = listeners.map(async (listener) => { + await listener(event, data) + }) + await (Promise.all(promises) as unknown as Promise) + } +} + +/** + * @public + * @param status - + * @returns + */ +export async function setPlatformStatus (status: Status): Promise { + if (status.severity === Severity.ERROR) { + console.trace('Platform Error Status', status) + } + await broadcastEvent(PlatformEvent, status) +} + +/** + * @public + * @param status - + * @param promise - + * @returns + */ +export async function monitor (status: Status, promise: Promise): Promise { + void setPlatformStatus(status) // eslint-disable-line no-void + try { + const result = await promise + void setPlatformStatus(OK) // eslint-disable-line no-void + return result + } catch (err) { + void setPlatformStatus(unknownError(err)) // eslint-disable-line no-void + console.error(err) + throw err + } +} diff --git a/foundations/core/packages/platform/src/i18n.ts b/foundations/core/packages/platform/src/i18n.ts new file mode 100644 index 0000000000..4534f01139 --- /dev/null +++ b/foundations/core/packages/platform/src/i18n.ts @@ -0,0 +1,246 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { IntlMessageFormat } from 'intl-messageformat' +import { setPlatformStatus } from './event' +import { _IdInfo, _parseId } from './ident' +import type { IntlString, Plugin } from './platform' +import { Severity, Status, unknownError } from './status' + +import { getMetadata } from './metadata' +import platform, { _EmbeddedId } from './platform' + +/** + * @public + */ +export type Loader = (locale: string) => Promise>> + +type Messages = Record> + +const loaders = new Map() +const translations = new Map>() +const cache = new Map>() +const englishTranslationsForMissing = new Map() +/** + * @public + * @param plugin - + * @param loader - + */ +export function addStringsLoader (plugin: Plugin, loader: Loader): void { + loaders.set(plugin, loader) +} + +/** + * Perform load of all internationalization sources for all plugins available. + * @public + */ +export async function loadPluginStrings (locale: string, force: boolean = false): Promise { + if (force) { + cache.clear() + } + for (const [plugin] of loaders) { + const localtTanslations = translations.get(locale) ?? new Map>() + if (!translations.has(locale)) { + translations.set(locale, localtTanslations) + } + let messages = localtTanslations.get(plugin) + if (messages === undefined || force) { + messages = await loadTranslationsForComponent(plugin, locale) + localtTanslations.set(plugin, messages) + } + } +} + +async function loadTranslationsForComponent (plugin: Plugin, locale: string): Promise { + const loader = loaders.get(plugin) + if (loader === undefined) { + const status = new Status(Severity.ERROR, platform.status.NoLoaderForStrings, { plugin }) + await setPlatformStatus(status) + return status + } + try { + return (await loader(locale)) as Record | Status + } catch (err) { + console.error('No translations found for plugin', plugin, err) + try { + return (await loader('en')) as Record | Status + } catch (err: any) { + const status = unknownError(err) + await setPlatformStatus(status) + return status + } + } +} + +function getCachedTranslation (id: _IdInfo, locale: string): IntlString | Status | undefined { + const localtTanslations = translations.get(locale) + if (localtTanslations === undefined) { + return undefined + } + const messages = localtTanslations.get(id.component) + if (messages === undefined) { + return undefined + } + if (messages instanceof Status) { + return messages + } + if (id.kind !== undefined) { + if ((messages[id.kind] as Record)?.[id.name] !== undefined) { + return (messages[id.kind] as Record)?.[id.name] + } + } +} + +async function getTranslation (id: _IdInfo, locale: string): Promise { + try { + const localtTanslations = translations.get(locale) ?? new Map>() + if (!translations.has(locale)) { + translations.set(locale, localtTanslations) + } + let messages = localtTanslations.get(id.component) + if (messages === undefined) { + messages = await loadTranslationsForComponent(id.component, locale) + localtTanslations.set(id.component, messages) + } + if (messages instanceof Status) { + return messages + } + if (id.kind !== undefined) { + if ((messages[id.kind] as Record)?.[id.name] !== undefined) { + return (messages[id.kind] as Record)?.[id.name] + } else { + let eng = englishTranslationsForMissing.get(id.component) + if (eng === undefined) { + eng = await loadTranslationsForComponent(id.component, 'en') + englishTranslationsForMissing.set(id.component, eng) + } + if (eng instanceof Status) { + return eng + } + return (eng[id.kind] as Record)?.[id.name] + } + } else { + return messages[id.name] as IntlString + } + } catch (err) { + const status = unknownError(err) + await setPlatformStatus(status) + return status + } +} + +/** + * @public + * @param message - + * @param params - + * @returns + */ +export async function translate

> ( + message: IntlString

, + params: P, + language?: string +): Promise { + const locale = language ?? getMetadata(platform.metadata.locale) ?? 'en' + const localCache = cache.get(locale) ?? new Map() + if (!cache.has(locale)) { + cache.set(locale, localCache) + } + const compiled = localCache.get(message) + + if (compiled !== undefined) { + if (compiled instanceof Status) { + return message + } + return compiled.format(params) + } else { + try { + const id = _parseId(message) + if (id.component === _EmbeddedId) { + return id.name + } + const translation = getCachedTranslation(id, locale) ?? (await getTranslation(id, locale)) ?? message + if (translation instanceof Status) { + localCache.set(message, translation) + return message + } + const compiled = new IntlMessageFormat(translation, locale, undefined, { ignoreTag: true }) + localCache.set(message, compiled) + return compiled.format(params) + } catch (err) { + const status = unknownError(err) + void setPlatformStatus(status) + localCache.set(message, status) + return message + } + } +} +/** + * Will do a translation in case language file already in cache, a translate is called and Promise is returned overwise + */ +export function translateCB

> ( + message: IntlString

, + params: P, + language: string | undefined, + resolve: (value: string) => void +): void { + const locale = language ?? getMetadata(platform.metadata.locale) ?? 'en' + const localCache = cache.get(locale) ?? new Map() + if (!cache.has(locale)) { + cache.set(locale, localCache) + } + const compiled = localCache.get(message) + + if (compiled !== undefined) { + if (compiled instanceof Status) { + resolve(message) + return + } + resolve(compiled.format(params)) + } else { + let id: _IdInfo + try { + id = _parseId(message) + if (id.component === _EmbeddedId) { + resolve(id.name) + return + } + } catch (err) { + const status = unknownError(err) + void setPlatformStatus(status) + localCache.set(message, status) + resolve(message) + return + } + const translation = getCachedTranslation(id, locale) + if (translation === undefined || translation instanceof Status) { + void translate(message, params, language) + .then((res) => { + resolve(res) + }) + .catch((err) => { + const status = unknownError(err) + void setPlatformStatus(status) + localCache.set(message, status) + resolve(message) + }) + return + } + + const compiled = new IntlMessageFormat(translation, locale, undefined, { ignoreTag: true }) + localCache.set(message, compiled) + resolve(compiled.format(params)) + } +} diff --git a/foundations/core/packages/platform/src/ident.ts b/foundations/core/packages/platform/src/ident.ts new file mode 100644 index 0000000000..52caa1d9ff --- /dev/null +++ b/foundations/core/packages/platform/src/ident.ts @@ -0,0 +1,43 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Id, Plugin } from './platform' +import { PlatformError, Status, Severity } from './status' +import platform, { _ID_SEPARATOR } from './platform' + +/** + * @internal + */ +export interface _IdInfo { + component: Plugin + kind: string + name: string +} + +/** + * @internal + */ +export function _parseId (id: Id): _IdInfo { + const path = id.split(_ID_SEPARATOR) + if (path.length < 3) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.InvalidId, { id })) + } + return { + component: path[0] as Plugin, + kind: path[1], + name: path.slice(2).join(_ID_SEPARATOR) + } +} diff --git a/foundations/core/packages/platform/src/index.ts b/foundations/core/packages/platform/src/index.ts new file mode 100644 index 0000000000..3d87bcb723 --- /dev/null +++ b/foundations/core/packages/platform/src/index.ts @@ -0,0 +1,37 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Metadata } from './metadata' + +export * from './event' +export * from './i18n' +export * from './metadata' +export * from './platform' +export * from './ident' +export { default } from './platform' +export * from './resource' +export * from './status' +export * from './testUtils' + +/** + * @public + */ +export type URL = string + +/** + * @public + */ +export type Asset = Metadata diff --git a/foundations/core/packages/platform/src/metadata.ts b/foundations/core/packages/platform/src/metadata.ts new file mode 100644 index 0000000000..df8774f7e8 --- /dev/null +++ b/foundations/core/packages/platform/src/metadata.ts @@ -0,0 +1,70 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Id } from './platform' + +/** + * Platform Metadata Identifier (PMI). + * + * 'Metadata' is simply any JavaScript object, which is used to configure platform, e.g. IP addresses. + * Another example of metadata is an asset URL. The logic behind providing asset URLs as metadata is + * we know URL at compile time only and URLs vary depending on deployment options. + * + * @public + */ +export type Metadata = Id & { __metadata: T } + +/** + * @public + */ +export type ExtractType>> = { + [P in keyof X]: X[P] extends Metadata ? Z : never +} + +const metadata = new Map, any>() + +/** + * @public + * @param id - + * @returns + */ +export function getMetadata (id: Metadata): T | undefined { + return metadata.get(id) +} + +/** + * @public + * @param id - + * @param value - + */ +export function setMetadata (id: Metadata, value: T): void { + metadata.set(id, value) +} + +/** + * @public + * @param ids - + * @param data - + */ +export function loadMetadata>> (ids: X, data: ExtractType): void { + for (const key in ids) { + const id = ids[key] + const resource = data[key] + if (resource === undefined) { + throw new Error(`no metadata provided, key: ${key}, id: ${String(id)}`) + } + metadata.set(id, resource) + } +} diff --git a/foundations/core/packages/platform/src/platform.ts b/foundations/core/packages/platform/src/platform.ts new file mode 100644 index 0000000000..9e61edc9c6 --- /dev/null +++ b/foundations/core/packages/platform/src/platform.ts @@ -0,0 +1,185 @@ +/*! +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +*/ +import { Metadata, PluginLoader, PluginModule, Resources } from '.' + +/** + * Id in format 'plugin.resource-kind.id' + * + * @public + */ +export type Id = string & { __id: true } + +/** + * Plugin Id + * + * @public + */ +export type Plugin = string & { __plugin: true } + +/** + * Platform Resource Identifier (PRI) + * + * @remarks + * + * Almost anything in the Anticrm Platform is a `Resource`. Resources referenced by Platform Resource Identifier (PRI). + * + * @example + * ```typescript + * `core.string.ClassLabel` as Resource // translated string according to current language and i18n settings + * `workbench.icon.Add` as Resource // URL to SVG sprites + * ``` + * + * @public + */ +export type Resource = Id & { __resource: T } + +/** + * Internationalized string Id + * + * @public + */ +export type IntlString = any> = Id & { __intl_string: T } + +/** + * Status Code. Also works as i18n string Id for status description. + * + * @public + */ +export type StatusCode = any> = IntlString + +/** + * @public + */ +export type Namespace = Record> + +/** + * @internal + */ +export const _ID_SEPARATOR = ':' + +/** + * @internal + */ +export const _EmbeddedId = 'embedded' + +function identify (result: Record, prefix: string, namespace: Record): Namespace { + for (const key in namespace) { + const value = namespace[key] + if (typeof result[key] === 'string') { + throw new Error(`'identify' overwrites '${key}' for ${prefix}`) + } + const ident = prefix + _ID_SEPARATOR + key + result[key] = typeof value === 'string' ? ident : identify(result[key] ?? {}, ident, value) + } + return result +} + +/** + * @public + */ +export function getEmbeddedLabel (str: string): IntlString { + return (_EmbeddedId + _ID_SEPARATOR + _EmbeddedId + _ID_SEPARATOR + str) as IntlString +} + +/** + * Defines plugin Ids. + * + * @public + * @param plugin - + * @param namespace - + * @returns + */ +export function plugin (plugin: Plugin, namespace: N): N { + return identify({}, plugin, namespace) as N +} + +/** + * Merges plugin Ids with Ids provided. + * + * @public + * @param plugin - + * @param ns - + * @param merge - + * @returns + */ +export function mergeIds (plugin: Plugin, ns: N, merge: M): N & M { + return identify({ ...ns }, plugin, merge) as N & M +} + +/** + * @public + */ +export const platformId = 'platform' as Plugin + +export default plugin(platformId, { + status: { + OK: '' as StatusCode, + BadError: '' as StatusCode, + UnknownError: '' as StatusCode<{ message: string }>, + InvalidId: '' as StatusCode<{ id: string }>, + ConnectionClosed: '' as StatusCode, + + LoadingPlugin: '' as StatusCode<{ plugin: string }>, + NoLocationForPlugin: '' as StatusCode<{ plugin: Plugin }>, + ResourceNotFound: '' as StatusCode<{ resource: Resource }>, + + NoLoaderForStrings: '' as StatusCode<{ plugin: Plugin }>, + + BadRequest: '' as StatusCode, + Forbidden: '' as StatusCode, // 403 + Unauthorized: '' as StatusCode, // 401 + TokenExpired: '' as StatusCode, // 401 + TokenNotActive: '' as StatusCode<{ notBefore: number }>, // 401 + Conflict: '' as StatusCode, // 409 + ExpiredLink: '' as StatusCode, + UnknownMethod: '' as StatusCode<{ method: string }>, + InternalServerError: '' as StatusCode, + MaintenanceWarning: '' as StatusCode<{ time: number, message?: string }>, + MaintenanceWarningTime: '' as IntlString, + AccountNotFound: '' as StatusCode<{ account?: string }>, + AccountMismatch: '' as StatusCode<{ account?: string, requiredAccount?: string }>, + AccountNotConfirmed: '' as StatusCode, + WorkspaceNotFound: '' as StatusCode<{ workspaceUuid?: string, workspaceName?: string, workspaceUrl?: string }>, + WorkspaceArchived: '' as StatusCode<{ workspaceUuid: string }>, + WorkspaceMigration: '' as StatusCode<{ workspaceUuid: string }>, + SocialIdNotFound: '' as StatusCode<{ value?: string, type?: string, _id?: string }>, + SocialIdNotConfirmed: '' as StatusCode<{ socialId: string, type: string }>, + SocialIdAlreadyConfirmed: '' as StatusCode<{ socialId: string, type: string }>, + IntegrationExists: '' as StatusCode, + IntegrationAlreadyExists: '' as StatusCode, + IntegrationNotFound: '' as StatusCode, + IntegrationSecretAlreadyExists: '' as StatusCode, + IntegrationSecretNotFound: '' as StatusCode, + PersonNotFound: '' as StatusCode<{ person: string }>, + InvalidPassword: '' as StatusCode<{ account: string }>, + PasswordLoginLocked: '' as StatusCode, + AccountAlreadyExists: '' as StatusCode, + WorkspaceAlreadyExists: '' as StatusCode<{ workspace: string }>, + WorkspaceRateLimit: '' as StatusCode<{ workspace: string }>, + WorkspaceLimitReached: '' as StatusCode<{ workspace: string }>, + InvalidOtp: '' as StatusCode, + InviteNotFound: '' as StatusCode<{ email: string }>, + MailboxError: '' as StatusCode<{ reason: string }>, + SocialIdAlreadyExists: '' as StatusCode, + ReadOnlyAccount: '' as StatusCode, + RegularAccount: '' as StatusCode, + SystemAccount: '' as StatusCode + }, + metadata: { + locale: '' as Metadata, + LoadHelper: '' as Metadata<(loader: PluginLoader) => Promise>> + } +}) diff --git a/foundations/core/packages/platform/src/resource.ts b/foundations/core/packages/platform/src/resource.ts new file mode 100644 index 0000000000..e679a8e32c --- /dev/null +++ b/foundations/core/packages/platform/src/resource.ts @@ -0,0 +1,170 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { monitor } from './event' +import { _parseId } from './ident' +import type { Plugin, Resource } from './platform' +import { PlatformError, Severity, Status } from './status' + +import { getMetadata } from './metadata' +import platform from './platform' + +/** + * @public + */ +export type Resources = Record> + +/** + * @public + */ +export interface PluginModule { + default: () => Promise +} + +/** + * @public + */ +export type PluginLoader = () => Promise> + +const locations = new Map>() + +/** + * @public + * @param plugin - + * @param module - + */ +export function addLocation (plugin: Plugin, module: PluginLoader): void { + locations.set(plugin, module) +} + +/** + * @public + * return list of registred plugins. + */ +export function getPlugins (): Plugin[] { + return Array.from(locations.keys()) +} + +function getLocation (plugin: Plugin): PluginLoader { + const location = locations.get(plugin) + if (location === undefined) { + throw new PlatformError( + new Status(Severity.ERROR, platform.status.NoLocationForPlugin, { + plugin + }) + ) + } + return location +} + +const loading = new Map>() + +function loadPlugin (id: Plugin): Resources | Promise { + let pluginLoader = loading.get(id) + if (pluginLoader === undefined) { + const status = new Status(Severity.INFO, platform.status.LoadingPlugin, { + plugin: id + }) + + const loadHelper = getMetadata(platform.metadata.LoadHelper) + + const locationLoader = getLocation(id) + pluginLoader = monitor(status, loadHelper !== undefined ? loadHelper(locationLoader) : locationLoader()).then( + async (plugin) => { + try { + // In case of ts-node, we have a bit different import structure, so let's check for it. + if (typeof plugin.default === 'object') { + // eslint-disable-next-line @typescript-eslint/return-await + return await (plugin as any).default.default() + } + return await plugin.default() + } catch (err: any) { + console.error(err) + throw err + } + } + ) + loading.set(id, pluginLoader) + } + return pluginLoader +} + +const cachedResource = new Map() + +/** + * @public + * @param resource - + * @returns + */ +export async function getResource (resource: Resource): Promise { + const cached = cachedResource.get(resource) + if (cached !== undefined) { + return cached + } + const info = _parseId(resource) + let resources = loading.get(info.component) ?? loadPlugin(info.component) + if (resources instanceof Promise) { + resources = await resources + loading.set(info.component, resources) + } + const value = resources[info.kind]?.[info.name] + if (value === undefined) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.ResourceNotFound, { resource })) + } + cachedResource.set(resource, value) + return value +} + +/** + * @public + * @param resource - + * @returns + */ +export function getResourceP (resource: Resource): T | Promise { + return cachedResource.get(resource) ?? getResource(resource) +} + +/** + * @public + * @param resource - + * @returns + */ +export function getResourceC (resource: Resource | undefined, callback: (resource: T | undefined) => void): void { + if (resource === undefined) { + callback(undefined) + return + } + const cached = cachedResource.get(resource) + if (cached !== undefined) { + callback(cached) + } else { + void getResource(resource) + .then((r) => { + callback(r) + }) + .catch(() => { + callback(undefined) + }) + } +} + +/** + * @public + */ +export function getResourcePlugin (resource: Resource): Plugin { + const info = _parseId(resource) + return info.component +} diff --git a/foundations/core/packages/platform/src/status.ts b/foundations/core/packages/platform/src/status.ts new file mode 100644 index 0000000000..807a0d79ba --- /dev/null +++ b/foundations/core/packages/platform/src/status.ts @@ -0,0 +1,100 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/** + * Anticrm Platform Foundation Types + * @packageDocumentation + */ + +import type { StatusCode } from './platform' +import platform from './platform' + +/** + * Status severity + * @public + */ +export enum Severity { + OK = 'OK', + INFO = 'INFO', + WARNING = 'WARNING', + ERROR = 'ERROR' +} + +/** + * Status of an operation + * @public + */ +export class Status

= any> { + readonly severity: Severity + readonly code: StatusCode

+ readonly params: P + + constructor (severity: Severity, code: StatusCode

, params: P) { + this.severity = severity + this.code = code + this.params = params + } +} + +/** + * Error object wrapping `Status` + * @public + */ +export class PlatformError

> extends Error { + readonly status: Status

+ + constructor (status: Status

) { + super(`${status.severity}: ${status.code} ${JSON.stringify(status.params)}`) + this.status = status + } +} + +/** + * OK Status + * @public + */ +export const OK = new Status(Severity.OK, platform.status.OK, {}) + +/** + * Error Status + * @public + */ +export const ERROR = new Status(Severity.ERROR, platform.status.BadError, {}) + +/** + * Error Status for Unauthorized + * @public + */ +export const UNAUTHORIZED = new Status(Severity.ERROR, platform.status.Unauthorized, {}) + +/** + * @public + * @param message - + * @returns + */ +export function unknownStatus (message: string): Status { + return new Status(Severity.ERROR, platform.status.UnknownError, { message }) +} + +/** + * Creates unknown error status + * @public + */ +export function unknownError (err: unknown): Status { + if (err instanceof PlatformError) return err.status + if (err instanceof Error) return unknownStatus(err.message) + if (typeof err === 'string') return unknownStatus(err) + return ERROR +} diff --git a/foundations/core/packages/platform/src/testUtils.ts b/foundations/core/packages/platform/src/testUtils.ts new file mode 100644 index 0000000000..ac431aa993 --- /dev/null +++ b/foundations/core/packages/platform/src/testUtils.ts @@ -0,0 +1,28 @@ +import { Loader } from './i18n' + +function makeLocaleMatcher (target: object): object { + return Object.entries(target).reduce( + (obj, [key, value]) => ({ + ...obj, + [key]: typeof value === 'string' ? expect.any(String) : makeLocaleMatcher(value) + }), + {} + ) +} + +const langs = ['en', 'ru'] + +/** + * @public + * @param loader - + * @returns + */ +export function makeLocalesTest (loader: Loader) { + return async () => { + const [target, ...rest] = await Promise.all(langs.map(loader)) + const matcher = makeLocaleMatcher(target) + rest.forEach((loc) => { + expect(loc).toEqual(matcher) + }) + } +} diff --git a/foundations/core/packages/platform/tsconfig.json b/foundations/core/packages/platform/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/platform/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/postgres-base/.eslintrc.js b/foundations/core/packages/postgres-base/.eslintrc.js new file mode 100644 index 0000000000..ce90fb9646 --- /dev/null +++ b/foundations/core/packages/postgres-base/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/postgres-base/.npmignore b/foundations/core/packages/postgres-base/.npmignore new file mode 100644 index 0000000000..9b083cb0c4 --- /dev/null +++ b/foundations/core/packages/postgres-base/.npmignore @@ -0,0 +1,8 @@ +* +!/lib/** +!/types/** +!/src/** +!CHANGELOG.md +/lib/**/__test__/ +/types/**/__test__/ +/src/**/__test__/ \ No newline at end of file diff --git a/foundations/core/packages/postgres-base/CHANGELOG.json b/foundations/core/packages/postgres-base/CHANGELOG.json new file mode 100644 index 0000000000..3f964c9c17 --- /dev/null +++ b/foundations/core/packages/postgres-base/CHANGELOG.json @@ -0,0 +1,51 @@ +{ + "name": "@hcengineering/postgres-base", + "entries": [ + { + "version": "0.7.12", + "tag": "@hcengineering/postgres-base_v0.7.12", + "date": "Sat, 11 Oct 2025 19:18:56 GMT", + "comments": { + "patch": [ + { + "comment": "rollback eslint" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.14` to `0.7.15`" + } + ] + } + }, + { + "version": "0.7.11", + "tag": "@hcengineering/postgres-base_v0.7.11", + "date": "Sat, 11 Oct 2025 17:58:53 GMT", + "comments": { + "patch": [ + { + "comment": "Fix eslint deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.12` to `0.7.13`" + } + ] + } + }, + { + "version": "0.7.10", + "tag": "@hcengineering/postgres-base_v0.7.10", + "date": "Fri, 10 Oct 2025 12:32:59 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.10` to `0.7.11`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/postgres-base/CHANGELOG.md b/foundations/core/packages/postgres-base/CHANGELOG.md new file mode 100644 index 0000000000..f81c959a6a --- /dev/null +++ b/foundations/core/packages/postgres-base/CHANGELOG.md @@ -0,0 +1,23 @@ +# Change Log - @hcengineering/postgres-base + +This log was last generated on Sat, 11 Oct 2025 19:18:56 GMT and should not be manually modified. + +## 0.7.12 +Sat, 11 Oct 2025 19:18:56 GMT + +### Patches + +- rollback eslint + +## 0.7.11 +Sat, 11 Oct 2025 17:58:53 GMT + +### Patches + +- Fix eslint deps + +## 0.7.10 +Fri, 10 Oct 2025 12:32:59 GMT + +_Initial release_ + diff --git a/foundations/core/packages/postgres-base/config/rig.json b/foundations/core/packages/postgres-base/config/rig.json new file mode 100644 index 0000000000..78cc5a1733 --- /dev/null +++ b/foundations/core/packages/postgres-base/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "node" +} diff --git a/foundations/core/packages/postgres-base/jest.config.js b/foundations/core/packages/postgres-base/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/foundations/core/packages/postgres-base/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/foundations/core/packages/postgres-base/package.json b/foundations/core/packages/postgres-base/package.json new file mode 100644 index 0000000000..923f665fda --- /dev/null +++ b/foundations/core/packages/postgres-base/package.json @@ -0,0 +1,59 @@ +{ + "name": "@hcengineering/postgres-base", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Copyright © Hardcore Engineering Inc.", + "template": "@hcengineering/node-package", + "license": "EPL-2.0", + "files": [ + "lib/**/*", + "!lib/**/__test__/**", + "types/**/*", + "!types/**/__test__/**", + "src/**/*", + "!src/**/__test__/**", + "README.md", + "CHANGELOG.md" + ], + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --forceExit", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/node": "^22.18.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0" + }, + "dependencies": { + "postgres": "^3.4.7" + }, + "repository": "https://github.com/hcengineering/huly.utils", + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + }, + "publishConfig": { + "access": "public" + } +} diff --git a/foundations/core/packages/postgres-base/src/index.ts b/foundations/core/packages/postgres-base/src/index.ts new file mode 100644 index 0000000000..4f820c16be --- /dev/null +++ b/foundations/core/packages/postgres-base/src/index.ts @@ -0,0 +1,439 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import postgres, { type Options, type ParameterOrJSON } from 'postgres' + +const clientRefs = new Map() + +let clId = 0 + +export type DBResult = any[] & { count: number } +export interface DBClient { + execute: (query: string, parameters?: ParameterOrJSON[] | undefined) => Promise + + release: () => void + + reserve: () => Promise + + raw: () => postgres.Sql +} + +export function createDBClient (client: postgres.Sql, release: () => void = () => {}): DBClient { + return { + execute: (query, parameters) => + client.unsafe(query, doFetchTypes ? parameters : convertArrayParams(parameters), getPrepare()), + release, + reserve: async () => { + const reserved = await client.reserve() + return createDBClient(reserved, () => { + reserved.release() + }) + }, + raw: () => client + } +} + +export function convertArrayParams (params?: unknown[]): any[] | undefined { + if (params === undefined) return undefined + + return params.map((param) => { + if (!Array.isArray(param)) return param + + if (param.length === 0) return '{}' + + const sanitized = param.map((item) => { + if (item === null || item === undefined) return 'NULL' + + if (typeof item === 'number' || typeof item === 'boolean') { + return String(item) + } + + if (typeof item === 'string') { + const escaped = item.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + return `"${escaped}"` + } + + const json = JSON.stringify(item) + const escapedJson = json.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + return `"${escapedJson}"` + }) + + return `{${sanitized.join(',')}}` + }) +} + +export async function retryTxn ( + pool: postgres.Sql, + operation: (client: postgres.TransactionSql) => Promise +): Promise { + await pool.begin(async (client) => { + const result = await operation(client) + return result + }) +} + +/** + * @public + */ +export async function shutdownPostgres (): Promise { + for (const c of connections.values()) { + c.close(true) + } + connections.clear() +} + +export interface PostgresClientReference { + getClient: () => Promise + + mgr: ConnectionMgr + close: () => void + url: () => string +} + +class PostgresClientReferenceImpl { + count: number + client: postgres.Sql + + mgr: ConnectionMgr + + constructor ( + readonly connectionString: string, + client: postgres.Sql, + readonly onclose: () => void + ) { + this.count = 0 + this.client = client + this.mgr = new ConnectionMgr(createDBClient(this.client)) + } + + url (): string { + return this.connectionString + } + + getClient (): postgres.Sql { + return this.client + } + + close (force: boolean = false): void { + this.count-- + if (this.count === 0 || force) { + if (force) { + this.count = 0 + } + void (async () => { + this.mgr.close() + this.onclose() + const cl = this.client + await cl.end({ timeout: 1 }) + })() + } + } + + addRef (): void { + this.count++ + } +} +export class ClientRef implements PostgresClientReference { + id = ++clId + constructor ( + readonly client: PostgresClientReferenceImpl, + readonly mgr: ConnectionMgr + ) { + clientRefs.set(this.id, this) + } + + url (): string { + return this.client.url() + } + + closed = false + async getClient (): Promise { + if (!this.closed) { + return this.client.getClient() + } else { + throw Error('DB client is already closed') + } + } + + close (): void { + // Do not allow double close of mongo connection client + if (!this.closed) { + clientRefs.delete(this.id) + this.closed = true + this.client.close() + } + } +} + +export let dbExtraOptions: Partial> = {} +export function setDBExtraOptions (options: Partial>): void { + dbExtraOptions = options +} + +export function getPrepare (): { prepare: boolean } { + return { prepare: dbExtraOptions.prepare ?? false } +} + +export const doFetchTypes = true + +const connections = new Map() + +/** + * Initialize a connection to DB + * @public + */ +export function getDBClient ( + connectionString: string, + database?: string, + serviceName: string = 'transactor' +): PostgresClientReference { + const extraOptions = JSON.parse(process.env.POSTGRES_OPTIONS ?? '{}') + const key = `${connectionString}${extraOptions}` + + let existing = connections.get(key) + + if (existing === undefined) { + const sql = postgres(connectionString, { + connection: { + application_name: serviceName + }, + database, + max: 10, + min: 2, + connect_timeout: 30, + idle_timeout: 0, + transform: { + undefined: null + }, + debug: false, + notice: false, + onnotice (notice) {}, + onparameter (key, value) {}, + ...dbExtraOptions, + ...extraOptions, + fetch_types: doFetchTypes + }) + + existing = new PostgresClientReferenceImpl(connectionString, sql, () => { + connections.delete(key) + }) + connections.set(key, existing) + } + // Add reference and return once closable + existing.addRef() + return new ClientRef(existing, existing.mgr) +} + +class ConnectionInfo { + // It should preserve at least one available connection in pool, other connection should be closed + available: DBClient[] = [] + + released: boolean = false + + constructor ( + readonly connectionId: string, + protected readonly client: DBClient, + readonly managed: boolean, + readonly mgrId: string + ) {} + + async withReserve (action: (reservedClient: DBClient) => Promise, forced: boolean = false): Promise { + let reserved: DBClient | undefined + + // Check if we have at least one available connection and reserve one more if required. + if (this.available.length === 0) { + if (this.managed || forced) { + reserved = await this.client.reserve() + } + } else { + reserved = this.available.shift() as DBClient + } + + try { + // Use reserved or pool + return await action(reserved ?? this.client) + } catch (err: any) { + console.error(err) + throw err + } finally { + if (this.released) { + try { + reserved?.release() + } catch (err: any) { + console.error('failed to release', err) + } + } else if (reserved !== undefined) { + if (this.available.length > 0) { + reserved?.release() + } else { + this.available.push(reserved) + } + } + } + } + + release (): void { + for (const c of [...this.available]) { + c.release() + } + this.available = [] + } +} + +export class ConnectionMgr { + private readonly connections = new Map() + constructor (protected readonly client: DBClient) {} + + async write (id: string | undefined, mgrId: string, fn: (client: DBClient) => Promise): Promise { + const backoffInterval = 25 // millis + const maxTries = 5 + let tries = 0 + + const realId = id ?? `${++clId}` + + const connection = this.getConnection(realId, mgrId, false) + + try { + while (true) { + const retry: boolean | Error = await connection.withReserve(async (client) => { + tries++ + try { + await client.execute('BEGIN;') + await fn(client) + await client.execute('COMMIT;') + return true + } catch (err: any) { + await client.execute('ROLLBACK;') + console.error({ message: 'failed to process tx', error: err.message, cause: err }) + + if (!this.isRetryableError(err) || tries === maxTries) { + return err + } else { + console.log('Transaction failed. Retrying.') + console.log(err.message) + return false + } + } + }, true) + if (retry === true) { + break + } + if (retry instanceof Error) { + // Pass it to exit + throw retry + } + // Retry for a timeout + await new Promise((resolve) => setTimeout(resolve, backoffInterval)) + } + } finally { + if (!connection.managed) { + // We need to relase in case it temporaty connection was used + connection.release() + } + } + } + + async retry (id: string | undefined, mgrId: string, fn: (client: DBClient) => Promise): Promise { + const backoffInterval = 25 // millis + const maxTries = 5 + let tries = 0 + + const realId = id ?? `${++clId}` + // Will reuse reserved if had and use new one if not + const connection = this.getConnection(realId, mgrId, false) + + try { + while (true) { + const retry: false | { result: any } | Error = await connection.withReserve(async (client) => { + tries++ + try { + return { result: await fn(client) } + } catch (err: any) { + console.error({ message: 'failed to process sql', error: err.message, cause: err }) + if (!this.isRetryableError(err) || tries === maxTries) { + return err + } else { + console.log('Read Transaction failed. Retrying.') + console.log(err.message) + return false + } + } + }) + if (retry instanceof Error) { + // Pass it to exit + throw retry + } + if (retry === false) { + // Retry for a timeout + await new Promise((resolve) => setTimeout(resolve, backoffInterval)) + continue + } + return retry.result + } + } finally { + if (!connection.managed) { + // We need to relase in case it temporaty connection was used + connection.release() + } + } + } + + release (id: string): void { + const conn = this.connections.get(id) + if (conn !== undefined) { + conn.released = true + this.connections.delete(id) // We need to delete first + conn.release() + } + } + + close (mgrId?: string): void { + const cnts = this.connections + for (const [k, conn] of Array.from(cnts.entries())) { + if (mgrId !== undefined && conn.mgrId !== mgrId) { + continue + } + cnts.delete(k) + try { + conn.release() + } catch (err: any) { + console.error('failed to release connection') + } + } + } + + getConnection (id: string, mgrId: string, managed: boolean = true): ConnectionInfo { + let conn = this.connections.get(id) + if (conn === undefined) { + conn = new ConnectionInfo(id, this.client, managed, mgrId) + } + if (managed) { + this.connections.set(id, conn) + } + return conn + } + + private isRetryableError (err: any): boolean { + const msg: string = err?.message ?? '' + + return ( + err.code === '40001' || // Retry transaction + err.code === '55P03' || // Lock not available + err.code === 'CONNECTION_CLOSED' || // This error is thrown if the connection was closed without an error. + err.code === 'CONNECTION_DESTROYED' || // This error is thrown for any queries that were pending when the timeout to sql.end({ timeout: X }) was reached. If the DB client is being closed completely retry will result in CONNECTION_ENDED which is not retried so should be fine. + msg.includes('RETRY_SERIALIZABLE') + ) + } +} diff --git a/foundations/core/packages/postgres-base/tsconfig.json b/foundations/core/packages/postgres-base/tsconfig.json new file mode 100644 index 0000000000..c6a877cf6c --- /dev/null +++ b/foundations/core/packages/postgres-base/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/query/.eslintrc.js b/foundations/core/packages/query/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/query/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/query/.npmignore b/foundations/core/packages/query/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/query/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/query/CHANGELOG.json b/foundations/core/packages/query/CHANGELOG.json new file mode 100644 index 0000000000..b943b991d3 --- /dev/null +++ b/foundations/core/packages/query/CHANGELOG.json @@ -0,0 +1,121 @@ +{ + "name": "@hcengineering/query", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/query_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.6", + "tag": "@hcengineering/query_v0.7.6", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + }, + { + "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/query_v0.7.5", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/query_v0.7.4", + "date": "Thu, 09 Oct 2025 16:57:55 GMT", + "comments": { + "patch": [ + { + "comment": "Fix bug in queue cleanup and add more tests to it" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/query_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/query_v0.6.1", + "date": "Sun, 08 Aug 2021 21:05:26 GMT", + "comments": { + "patch": [ + { + "comment": "Fix server connection" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `~0.6.3` to `~0.6.8`" + } + ] + } + }, + { + "version": "0.7.0", + "tag": "@hcengineering/query_v0.6.0", + "date": "Sun, 08 Aug 2021 10:14:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.3` to `~0.6.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/query/CHANGELOG.md b/foundations/core/packages/query/CHANGELOG.md new file mode 100644 index 0000000000..2350664087 --- /dev/null +++ b/foundations/core/packages/query/CHANGELOG.md @@ -0,0 +1,47 @@ +# Change Log - @hcengineering/query + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.6 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.5 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.4 +Thu, 09 Oct 2025 16:57:55 GMT + +### Patches + +- Fix bug in queue cleanup and add more tests to it + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Version update only_ + +## 0.7.0 +Sun, 08 Aug 2021 21:05:26 GMT + +### Patches + +- Fix server connection + +## 0.7.0 +Sun, 08 Aug 2021 10:14:57 GMT + +_Initial release_ + diff --git a/foundations/core/packages/query/config/rig.json b/foundations/core/packages/query/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/query/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/query/jest.config.js b/foundations/core/packages/query/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/query/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/query/package.json b/foundations/core/packages/query/package.json new file mode 100644 index 0000000000..bdccbbfdb5 --- /dev/null +++ b/foundations/core/packages/query/package.json @@ -0,0 +1,59 @@ +{ + "name": "@hcengineering/query", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "build:watch": "compile", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/platform": "workspace:^0.7.18", + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/analytics": "workspace:^0.7.17", + "fast-equals": "^5.2.2" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/query/src/__tests__/advanced-coverage.test.ts b/foundations/core/packages/query/src/__tests__/advanced-coverage.test.ts new file mode 100644 index 0000000000..bcb1046eaa --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/advanced-coverage.test.ts @@ -0,0 +1,488 @@ +// Advanced tests for complex LiveQuery scenarios +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import core, { createClient, Ref, SortingOrder, TxOperations } from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx) => { + void liveQuery.tx(...tx) + } + return { + liveQuery, + factory: new TxOperations(storage, core.account.System), + close: async () => { + await liveQuery.close() + } + } +} + +describe('LiveQuery Advanced Coverage Tests', () => { + it('should handle complex sorting scenarios', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create multiple documents + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'Z-space', + description: 'last', + private: false, + members: [], + archived: false + }) + + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'A-space', + description: 'first', + private: false, + members: [], + archived: false + }) + + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'M-space', + description: 'middle', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Query with sorting + liveQuery.query(core.class.Space, {}, callback, { sort: { name: SortingOrder.Ascending }, limit: 10 }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + const lastResult = callback.mock.calls[callback.mock.calls.length - 1][0] + expect(lastResult.length).toBeGreaterThan(0) + + await close() + }) + + it('should handle complex query with multiple conditions', async () => { + const { liveQuery, factory, close } = await getClient() + + const spaces = [] + for (let i = 0; i < 5; i++) { + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: `multi-${i}`, + description: `desc-${i}`, + private: i % 2 === 0, + members: [], + archived: i > 3 + }) + spaces.push(space) + } + + const callback = jest.fn() + + // Complex query + liveQuery.query(core.class.Space, { private: false, archived: false }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle findAll with complex options', async () => { + const { liveQuery, factory, close } = await getClient() + + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'findall-1', + description: 'test', + private: false, + members: [], + archived: false + }) + + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'findall-2', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Use findAll with various options + const result = await liveQuery.findAll( + core.class.Space, + { private: false }, + { + limit: 10, + sort: { modifiedOn: SortingOrder.Descending } + } + ) + + expect(result.length).toBeGreaterThan(0) + + await close() + }) + + it('should handle rapid document creation and updates', async () => { + const { liveQuery, factory, close } = await getClient() + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { private: false }, callback) + + await new Promise((resolve) => setTimeout(resolve, 50)) + + // Rapid creation + const spaces: Array> = [] + for (let i = 0; i < 10; i++) { + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: `rapid-${i}`, + description: 'test', + private: false, + members: [], + archived: false + }) + spaces.push(space) + } + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Rapid updates + for (const space of spaces) { + await factory.updateDoc(core.class.Space, core.space.Model, space, { + description: 'updated' + }) + } + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + + it('should handle document removal from live query', async () => { + const { liveQuery, factory, close } = await getClient() + + const space1 = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'remove-1', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { _id: space1 }, callback, { total: true }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + callback.mockClear() + + // Remove the document + await factory.removeDoc(core.class.Space, core.space.Model, space1) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Callback should be called with updated results + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle query with $in operator', async () => { + const { liveQuery, factory, close } = await getClient() + + const space1 = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'in-1', + description: 'test', + private: false, + members: [], + archived: false + }) + + const space2 = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'in-2', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { _id: { $in: [space1, space2] } }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle nested document updates', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'nested-update', + description: 'original', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { _id: space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + callback.mockClear() + + // Multiple rapid updates + await factory.updateDoc(core.class.Space, core.space.Model, space, { + description: 'update-1' + }) + + await factory.updateDoc(core.class.Space, core.space.Model, space, { + description: 'update-2' + }) + + await factory.updateDoc(core.class.Space, core.space.Model, space, { + description: 'update-3' + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle query with empty results', async () => { + const { liveQuery, close } = await getClient() + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { name: 'non-existent-document-name-12345' }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + const result = callback.mock.calls[0][0] + expect(result.length).toBe(0) + + await close() + }) + + it('should handle concurrent queries on same data', async () => { + const { liveQuery, factory, close } = await getClient() + + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'concurrent-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback1 = jest.fn() + const callback2 = jest.fn() + const callback3 = jest.fn() + + // Start multiple queries simultaneously + liveQuery.query(core.class.Space, { private: false }, callback1, { limit: 5 }) + liveQuery.query(core.class.Space, { private: false }, callback2, { limit: 10 }) + liveQuery.query(core.class.Space, { private: false }, callback3) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback1).toHaveBeenCalled() + expect(callback2).toHaveBeenCalled() + expect(callback3).toHaveBeenCalled() + + await close() + }) + + it('should handle total count in queries', async () => { + const { liveQuery, factory, close } = await getClient() + + for (let i = 0; i < 5; i++) { + await factory.createDoc(core.class.Space, core.space.Model, { + name: `total-${i}`, + description: 'test', + private: false, + members: [], + archived: false + }) + } + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { private: false }, callback, { limit: 2, total: true }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + const result = callback.mock.calls[callback.mock.calls.length - 1][0] + expect(result.total).toBeGreaterThanOrEqual(5) + + await close() + }) + + it('should handle query with archived documents', async () => { + const { liveQuery, factory, close } = await getClient() + + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'archived-1', + description: 'test', + private: false, + members: [], + archived: true + }) + + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'active-1', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Query for non-archived + liveQuery.query(core.class.Space, { archived: false }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle updating query multiple times', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'multi-update', + description: 'v1', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { _id: space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 50)) + + // Update multiple times + for (let i = 2; i <= 5; i++) { + await factory.updateDoc(core.class.Space, core.space.Model, space, { + description: `v${i}` + }) + await new Promise((resolve) => setTimeout(resolve, 30)) + } + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Should have been called multiple times + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + + it('should handle query unsubscribe and resubscribe cycle', async () => { + const { liveQuery, close } = await getClient() + + const callback1 = jest.fn() + + // Subscribe + const unsub1 = liveQuery.query(core.class.Space, {}, callback1) + + await new Promise((resolve) => setTimeout(resolve, 50)) + + // Unsubscribe + unsub1() + + await new Promise((resolve) => setTimeout(resolve, 50)) + + callback1.mockClear() + + const callback2 = jest.fn() + + // Resubscribe + liveQuery.query(core.class.Space, {}, callback2) + + await new Promise((resolve) => setTimeout(resolve, 50)) + + expect(callback2).toHaveBeenCalled() + + await close() + }) + + it('should handle findOne on non-existent document', async () => { + const { liveQuery, close } = await getClient() + + const result = await liveQuery.findOne(core.class.Space, { _id: 'non-existent-id-12345' as Ref }) + + expect(result).toBeUndefined() + + await close() + }) + + it('should handle mixed operations on same query', async () => { + const { liveQuery, factory, close } = await getClient() + + const spaces: Array> = [] + + // Create initial documents + for (let i = 0; i < 3; i++) { + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: `mixed-${i}`, + description: 'test', + private: false, + members: [], + archived: false + }) + spaces.push(space) + } + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { private: false }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Update one + await factory.updateDoc(core.class.Space, core.space.Model, spaces[0], { + description: 'updated' + }) + + // Create new one + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'mixed-new', + description: 'new', + private: false, + members: [], + archived: false + }) + + // Remove one + await factory.removeDoc(core.class.Space, core.space.Model, spaces[1]) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Should have been notified multiple times + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) +}) diff --git a/foundations/core/packages/query/src/__tests__/bug-hunting.test.ts b/foundations/core/packages/query/src/__tests__/bug-hunting.test.ts new file mode 100644 index 0000000000..72163f1559 --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/bug-hunting.test.ts @@ -0,0 +1,496 @@ +// Bug hunting tests - targeting specific edge cases and potential issues +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import core, { createClient, Ref, SortingOrder, TxOperations } from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' +import { test } from './minmodel' + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx) => { + void liveQuery.tx(...tx) + } + return { + liveQuery, + factory: new TxOperations(storage, core.account.System), + close: async () => { + await liveQuery.close() + } + } +} + +describe('Bug Hunting Tests', () => { + describe('Potential Bug: Sort order after updates with limit', () => { + it('should maintain correct sort order when updating document that exceeds limit', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create documents with specific sort order + const spaces: Array> = [] + for (let i = 0; i < 5; i++) { + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: `space-${String(i).padStart(2, '0')}`, + description: `priority-${i}`, + private: false, + members: [], + archived: false + }) + spaces.push(space) + } + + const callback = jest.fn() + + // Query with limit and sort + liveQuery.query(core.class.Space, {}, callback, { + limit: 3, + sort: { name: SortingOrder.Ascending } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialResult = callback.mock.calls[callback.mock.calls.length - 1][0] + expect(initialResult.length).toBeLessThanOrEqual(3) + + // Update a document outside the limit to see if it enters + await factory.updateDoc(core.class.Space, core.space.Model, spaces[4], { + name: 'space-00-updated' // Should now be first + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Check that result was updated correctly + const finalResult = callback.mock.calls[callback.mock.calls.length - 1][0] + expect(finalResult.length).toBeLessThanOrEqual(3) + + await close() + }) + }) + + describe('Potential Bug: Match function with mixins', () => { + it('should correctly match documents with mixin classes', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'mixin-match-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Query for the space + liveQuery.query(core.class.Space, { _id: space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Potential Bug: getCurrentDoc when document no longer matches', () => { + it('should handle document that stops matching query after update', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'match-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Query for private=false + liveQuery.query(core.class.Space, { _id: space, private: false }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + const initialResult = callback.mock.calls[callback.mock.calls.length - 1][0] + expect(initialResult.length).toBe(1) + + callback.mockClear() + + // Update to private=true, should no longer match + await factory.updateDoc(core.class.Space, core.space.Model, space, { + private: true + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Should have been called with empty results or document removed + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Potential Bug: Lookup updates with $push operations', () => { + it('should correctly handle $push operations on lookup fields', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'push-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'parent' + }) + + const callback = jest.fn() + + // Query with reverse lookup + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Add a child comment (this should trigger $push like behavior) + await factory.addCollection(test.class.TestComment, space, comment, test.class.TestComment, 'comments', { + message: 'child1' + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Should have been updated + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + }) + + describe('Potential Bug: Sort with nested operations', () => { + it('should detect need for re-sort when nested operations affect sort fields', async () => { + const { liveQuery, factory, close } = await getClient() + + const spaces: Array> = [] + for (let i = 0; i < 3; i++) { + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: `sort-${i}`, + description: 'test', + private: false, + members: [], + archived: false + }) + spaces.push(space) + } + + const callback = jest.fn() + + // Query with sort by name + liveQuery.query(core.class.Space, {}, callback, { + sort: { name: SortingOrder.Ascending } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Update name which should trigger re-sort + await factory.updateDoc(core.class.Space, core.space.Model, spaces[0], { + name: 'sort-zzz' // Should move to end + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + }) + + describe('Potential Bug: checkUpdatedDocMatch with limit', () => { + it('should refresh query when document stops matching and at limit', async () => { + const { liveQuery, factory, close } = await getClient() + + const spaces: Array> = [] + // Create more docs than limit + for (let i = 0; i < 5; i++) { + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: `limit-match-${i}`, + description: 'test', + private: false, + members: [], + archived: false + }) + spaces.push(space) + } + + const callback = jest.fn() + + // Query with limit + liveQuery.query(core.class.Space, { private: false }, callback, { + limit: 3 + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialResult = callback.mock.calls[callback.mock.calls.length - 1][0] + expect(initialResult.length).toBeLessThanOrEqual(3) + + // Make one of the documents not match + await factory.updateDoc(core.class.Space, core.space.Model, spaces[0], { + private: true + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Should have refreshed to include another document + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + }) + + describe('Potential Bug: updatedDocCallback with limit boundary', () => { + it('should handle updates when document is exactly at limit boundary', async () => { + const { liveQuery, factory, close } = await getClient() + + const spaces: Array> = [] + for (let i = 0; i < 4; i++) { + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: `boundary-${String(i).padStart(2, '0')}`, + description: 'test', + private: false, + members: [], + archived: false + }) + spaces.push(space) + } + + const callback = jest.fn() + + // Query with limit=3, we have 4 documents + liveQuery.query(core.class.Space, {}, callback, { + limit: 3, + sort: { name: SortingOrder.Ascending } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Update the 3rd document (at the limit boundary) + await factory.updateDoc(core.class.Space, core.space.Model, spaces[2], { + description: 'updated at boundary' + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + }) + + describe('Potential Bug: Reverse lookup with undefined values', () => { + it('should handle reverse lookups when field is undefined', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'undefined-lookup', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Query with reverse lookup that may have undefined fields + liveQuery.query(core.class.Space, { _id: space }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Potential Bug: Match with skipLookup flag', () => { + it('should correctly skip $lookup keys when matching', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'skip-lookup-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'test' + }) + + const callback = jest.fn() + + // Query with lookup + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { space: core.class.Space } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Potential Bug: Query with $search and updates', () => { + it('should handle updates on documents with $search query', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'searchable document', + description: 'findable content', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Query with search (though it won't actually search in mock) + liveQuery.query(core.class.Space, { $search: 'searchable' }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Update the document + await factory.updateDoc(core.class.Space, core.space.Model, space, { + description: 'updated content' + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Potential Bug: Multiple rapid unsubscribe/subscribe cycles', () => { + it('should handle rapid subscribe/unsubscribe cycles correctly', async () => { + const { liveQuery, close } = await getClient() + + const callbacks = [] + const unsubscribes = [] + + // Rapidly subscribe and unsubscribe + for (let i = 0; i < 10; i++) { + const callback = jest.fn() + callbacks.push(callback) + const unsub = liveQuery.query(core.class.Space, {}, callback) + unsubscribes.push(unsub) + + if (i % 2 === 0) { + unsub() // Unsubscribe every other one immediately + } + } + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Clean up remaining subscriptions + unsubscribes.forEach((unsub, i) => { + if (i % 2 !== 0) { + unsub() + } + }) + + await new Promise((resolve) => setTimeout(resolve, 50)) + + await close() + }) + }) + + describe('Potential Bug: Empty query result set operations', () => { + it('should handle operations on empty result sets', async () => { + const { liveQuery, factory, close } = await getClient() + + const callback = jest.fn() + + // Query that will have no results initially + liveQuery.query(core.class.Space, { name: 'does-not-exist-initially' }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + const initialResult = callback.mock.calls[callback.mock.calls.length - 1][0] + expect(initialResult.length).toBe(0) + + // Now create a matching document + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'does-not-exist-initially', + description: 'test', + private: false, + members: [], + archived: false + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const finalResult = callback.mock.calls[callback.mock.calls.length - 1][0] + expect(finalResult.length).toBe(1) + + await close() + }) + }) + + describe('Potential Bug: Mixin updates with reverse lookup', () => { + it('should handle mixin updates on documents with reverse lookups', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'mixin-reverse-lookup', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'test' + }) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Add child comment + await factory.addCollection(test.class.TestComment, space, comment, test.class.TestComment, 'comments', { + message: 'child' + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + }) +}) diff --git a/foundations/core/packages/query/src/__tests__/connection.ts b/foundations/core/packages/query/src/__tests__/connection.ts new file mode 100644 index 0000000000..6c3acc7b65 --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/connection.ts @@ -0,0 +1,180 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { + BackupClient, + Class, + Client, + ClientConnectEvent, + ClientConnection, + Doc, + DocChunk, + DocumentQuery, + Domain, + DOMAIN_TX, + FindOptions, + FindResult, + FulltextStorage, + generateId, + Hierarchy, + LoadModelResponse, + ModelDb, + Ref, + SearchOptions, + SearchQuery, + SearchResult, + Timestamp, + Tx, + TxDb, + TxResult, + type DomainParams, + type DomainRequestOptions, + type DomainResult, + type OperationDomain, + type TxHandler +} from '@hcengineering/core' +import { genMinModel } from './minmodel' + +export async function connect (handler: (tx: Tx) => void): Promise< +Client & +BackupClient & +FulltextStorage & { + isConnected: () => boolean + loadModel: (last: Timestamp, hash?: string) => Promise + pushHandler: (handler: TxHandler) => void +} +> { + const txes = genMinModel() + + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + + const transactions = new TxDb(hierarchy) + const model = new ModelDb(hierarchy) + for (const tx of txes) { + await transactions.tx(tx) + await model.tx(tx) + } + + class TestConnection implements ClientConnection { + private readonly hierarchy: Hierarchy + private readonly model: ModelDb + private readonly transactions: TxDb + + constructor (hierarchy: Hierarchy, model: ModelDb, transactions: TxDb) { + this.hierarchy = hierarchy + this.model = model + this.transactions = transactions + } + + isConnected (): boolean { + return true + } + + pushHandler (): void {} + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + const domain = this.hierarchy.getClass(_class).domain + if (domain === DOMAIN_TX) return await this.transactions.findAll(_class, query, options) + return await this.model.findAll(_class, query, options) + } + + async findOne( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise { + return (await this.findAll(_class, query, { ...options, limit: 1 })).shift() + } + + async domainRequest ( + domain: OperationDomain, + params: DomainParams, + options?: DomainRequestOptions + ): Promise { + return { domain, value: null } + } + + getHierarchy (): Hierarchy { + return this.hierarchy + } + + getModel (): ModelDb { + return this.model + } + + async tx (tx: Tx): Promise { + if (tx.objectSpace === core.space.Model) { + this.hierarchy.tx(tx) + } + await Promise.all([this.model.tx(tx), this.transactions.tx(tx)]) + handler(tx) + return {} + } + + async close (): Promise {} + + async loadChunk (domain: Domain, idx?: number): Promise { + return { + idx: -1, + docs: [], + finished: true + } + } + + async getDomainHash (domain: Domain): Promise { + return generateId() + } + + async loadModel (lastTxTime: Timestamp): Promise { + return txes + } + + async closeChunk (idx: number): Promise {} + + async loadDocs (domain: Domain, docs: Ref[]): Promise { + return [] + } + + async upload (domain: Domain, docs: Doc[]): Promise {} + + async clean (domain: Domain, docs: Ref[]): Promise {} + + async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { + return { docs: [] } + } + + async sendForceClose (): Promise {} + + handler?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise + + set onConnect ( + handler: ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined + ) { + this.handler = handler + void this.handler?.(ClientConnectEvent.Connected, '', {}) + } + + get onConnect (): ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined { + return this.handler + } + } + + return new TestConnection(hierarchy, model, transactions) +} diff --git a/foundations/core/packages/query/src/__tests__/deep-lookup.test.ts b/foundations/core/packages/query/src/__tests__/deep-lookup.test.ts new file mode 100644 index 0000000000..c75b416cd2 --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/deep-lookup.test.ts @@ -0,0 +1,502 @@ +// Deep lookup and association tests +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import core, { createClient, Ref, TxOperations } from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' +import { test } from './minmodel' + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx) => { + void liveQuery.tx(...tx) + } + return { + liveQuery, + factory: new TxOperations(storage, core.account.System), + close: async () => { + await liveQuery.close() + } + } +} + +describe('Deep Lookup Tests', () => { + describe('Nested lookup with missing parent', () => { + it('should handle nested lookup when parent document is undefined', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'nested-missing-parent', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Create a comment with attachedTo pointing to non-existent parent + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'orphan comment' + }) + + const callback = jest.fn() + + // Nested lookup where parent might not exist + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { + attachedTo: core.class.Space + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Reverse lookup with array values', () => { + it('should handle reverse lookup with custom attribute', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'reverse-custom-attr', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'parent' + }) + + const callback = jest.fn() + + // Reverse lookup with specific attribute (array format) + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { + _id: { + children: [test.class.TestComment, 'attachedTo'] // Custom attribute + } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + // Add child with custom attachedTo + await factory.addCollection(test.class.TestComment, space, comment, test.class.TestComment, 'comments', { + message: 'child' + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + }) + + describe('Reverse lookup with undefined or zero values', () => { + it('should skip reverse lookup fields that are undefined or 0', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'reverse-undefined', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // This should handle cases where the document might have undefined reverse lookup fields + liveQuery.query(core.class.Space, { _id: space }, callback, { + lookup: { + _id: { + // This might not exist on the document + somethingThatDoesNotExist: test.class.TestComment + } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Lookup with mixin keys', () => { + it('should handle lookup with mixin property keys', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'mixin-key-lookup', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'test' + }) + + const callback = jest.fn() + + // Lookup that will use checkMixinKey internally + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { + space: core.class.Space, + attachedTo: core.class.Space + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Complex nested lookup updates', () => { + it('should handle updates in nested lookup chain', async () => { + const { liveQuery, factory, close } = await getClient() + + const space1 = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'nested-space-1', + description: 'level 1', + private: false, + members: [], + archived: false + }) + + const comment1 = await factory.addCollection( + test.class.TestComment, + space1, + space1, + core.class.Space, + 'comments', + { + message: 'level 2' + } + ) + + const comment2 = await factory.addCollection( + test.class.TestComment, + space1, + comment1, + test.class.TestComment, + 'comments', + { + message: 'level 3' + } + ) + + const callback = jest.fn() + + // Deep nested lookup + liveQuery.query(test.class.TestComment, { _id: comment2 }, callback, { + lookup: { + attachedTo: [ + test.class.TestComment, + { + space: core.class.Space + } + ] + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Update the space (should propagate through nested lookup) + await factory.updateDoc(core.class.Space, core.space.Model, space1, { + description: 'level 1 updated' + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Lookup update with $pull operation', () => { + it('should handle $pull operations on lookup arrays', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'pull-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const childComments: Array> = [] + for (let i = 0; i < 3; i++) { + const child = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: `child-${i}` + } + ) + childComments.push(child) + } + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Remove a child comment (should trigger $pull-like behavior) + await factory.removeCollection( + test.class.TestComment, + space, + childComments[1], + parentComment, + test.class.TestComment, + 'comments' + ) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + }) + + describe('Lookup with attached doc updates', () => { + it('should handle reverse lookup updates when attached doc changes attachedTo', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'attached-move', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment1 = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'parent1' + }) + + const comment2 = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'parent2' + }) + + const childComment = await factory.addCollection( + test.class.TestComment, + space, + comment1, + test.class.TestComment, + 'comments', + { + message: 'child' + } + ) + + const callback = jest.fn() + + // Watch parent1 with reverse lookup + liveQuery.query(test.class.TestComment, { _id: comment1 }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Move child from comment1 to comment2 + await factory.updateCollection( + test.class.TestComment, + space, + childComment, + comment2, // New parent + test.class.TestComment, + 'comments', + { + attachedTo: comment2 + } + ) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Should have been notified about the change + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + }) + + describe('Lookup with single document vs array', () => { + it('should handle both single document and array lookups', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'single-vs-array', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'test' + }) + + const callback = jest.fn() + + // Single document lookup (space) and array lookup (_id.comments) + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { + space: core.class.Space, // Single document + _id: { comments: test.class.TestComment } // Array + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] + + // Should have both lookups + expect(result.$lookup).toBeDefined() + + await close() + }) + }) + + describe('Mixin class matching in queries', () => { + it('should handle queries on mixin classes', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'mixin-query', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Query on space class + liveQuery.query(core.class.Space, { _id: space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Lookup update edge cases', () => { + it('should handle updates to documents not matching current query', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'edge-lookup', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'test' + }) + + const callback = jest.fn() + + // Query specific comments + liveQuery.query(test.class.TestComment, { message: 'test' }, callback, { + lookup: { space: core.class.Space } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Update message so it no longer matches + await factory.updateCollection(test.class.TestComment, space, comment, space, core.class.Space, 'comments', { + message: 'updated-no-match' + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('GetObjectValue with nested properties', () => { + it('should handle lookups with nested property access', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'nested-property', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'test' + }) + + const callback = jest.fn() + + // Lookup that uses getObjectValue internally + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { + space: core.class.Space, + attachedTo: core.class.Space + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) +}) diff --git a/foundations/core/packages/query/src/__tests__/final-coverage.test.ts b/foundations/core/packages/query/src/__tests__/final-coverage.test.ts new file mode 100644 index 0000000000..1d239bb045 --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/final-coverage.test.ts @@ -0,0 +1,519 @@ +// Final comprehensive tests targeting uncovered scenarios +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import core, { createClient, Ref, SortingOrder, TxOperations } from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' +import { test } from './minmodel' + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx) => { + void liveQuery.tx(...tx) + } + return { + liveQuery, + factory: new TxOperations(storage, core.account.System), + close: async () => { + await liveQuery.close() + } + } +} + +describe('LiveQuery Final Coverage Tests', () => { + it('should handle complex update with attached documents', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create a space + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'attach-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Add attached comment + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'original' + }) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: comment }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Update the comment + await factory.updateCollection(test.class.TestComment, space, comment, space, core.class.Space, 'comments', { + message: 'updated' + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + + it('should handle reverse lookup updates on attached documents', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'reverse-lookup-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const callback = jest.fn() + + // Query with reverse lookup + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { + comments: test.class.TestComment + } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Add child comment + await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { + message: 'child1' + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Should have updated with new child + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + + it('should handle nested lookups with multiple levels', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'nested-lookup', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const childComment = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'child' + } + ) + + const callback = jest.fn() + + // Nested lookup + liveQuery.query(test.class.TestComment, { _id: childComment }, callback, { + lookup: { + attachedTo: [test.class.TestComment, { space: core.class.Space }] + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle query with limit and sorting together', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create multiple documents + for (let i = 0; i < 10; i++) { + await factory.createDoc(core.class.Space, core.space.Model, { + name: `sorted-${String(i).padStart(2, '0')}`, + description: 'test', + private: false, + members: [], + archived: false + }) + } + + const callback = jest.fn() + + // Query with limit and sort + liveQuery.query(core.class.Space, {}, callback, { + limit: 3, + sort: { name: SortingOrder.Ascending } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + const result = callback.mock.calls[callback.mock.calls.length - 1][0] + expect(result.length).toBeLessThanOrEqual(3) + + await close() + }) + + it('should handle projection with lookup', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'projection-lookup', + description: 'test description', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'test' + }) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + projection: { message: 1, _id: 1 }, + lookup: { space: core.class.Space } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle updates to documents with limit queries', async () => { + const { liveQuery, factory, close } = await getClient() + + const spaces: Array> = [] + + // Create documents + for (let i = 0; i < 5; i++) { + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: `limit-${i}`, + description: 'test', + private: false, + members: [], + archived: false + }) + spaces.push(space) + } + + const callback = jest.fn() + + // Query with limit + liveQuery.query(core.class.Space, {}, callback, { limit: 3 }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Update a document + await factory.updateDoc(core.class.Space, core.space.Model, spaces[0], { + description: 'updated' + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + + it('should handle removing attached documents', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'remove-attached', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'to be removed' + }) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: comment }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Remove the comment + await factory.removeCollection(test.class.TestComment, space, comment, space, core.class.Space, 'comments') + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + + it('should handle multiple simultaneous updates', async () => { + const { liveQuery, factory, close } = await getClient() + + const spaces: Array> = [] + + for (let i = 0; i < 3; i++) { + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: `multi-${i}`, + description: 'original', + private: false, + members: [], + archived: false + }) + spaces.push(space) + } + + const callback = jest.fn() + + liveQuery.query(core.class.Space, {}, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Simultaneous updates + await Promise.all( + spaces.map((space) => + factory.updateDoc(core.class.Space, core.space.Model, space, { + description: 'updated' + }) + ) + ) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + + it('should handle complex query conditions with $ne', async () => { + const { liveQuery, factory, close } = await getClient() + + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'not-this', + description: 'test', + private: false, + members: [], + archived: false + }) + + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'this-one', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { name: { $ne: 'not-this' } }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle attached document updates with reverse lookup', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'reverse-update', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const childComment = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'original child' + } + ) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Update child comment + await factory.updateCollection( + test.class.TestComment, + space, + childComment, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'updated child' + } + ) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + + it('should handle query reactivation from queue with updates', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'reactivate', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback1 = jest.fn() + + // Subscribe + const unsub = liveQuery.query(core.class.Space, { _id: space }, callback1) + + await new Promise((resolve) => setTimeout(resolve, 50)) + + // Unsubscribe to move to queue + unsub() + + await new Promise((resolve) => setTimeout(resolve, 50)) + + // Update while in queue + await factory.updateDoc(core.class.Space, core.space.Model, space, { + description: 'updated while queued' + }) + + await new Promise((resolve) => setTimeout(resolve, 50)) + + const callback2 = jest.fn() + + // Reactivate from queue + liveQuery.query(core.class.Space, { _id: space }, callback2) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback2).toHaveBeenCalled() + + await close() + }) + + it('should handle findAll with total option', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create more docs than limit + for (let i = 0; i < 10; i++) { + await factory.createDoc(core.class.Space, core.space.Model, { + name: `total-${i}`, + description: 'test', + private: false, + members: [], + archived: false + }) + } + + const result = await liveQuery.findAll( + core.class.Space, + {}, + { + limit: 5, + total: true + } + ) + + expect(result.length).toBe(5) + expect(result.total).toBeGreaterThanOrEqual(10) + + await close() + }) + + it('should handle complex attachedTo relationships', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'complex-attached', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment1 = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'comment1' + }) + + await factory.addCollection(test.class.TestComment, space, comment1, test.class.TestComment, 'comments', { + message: 'comment2' + }) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { attachedTo: comment1 }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) +}) diff --git a/foundations/core/packages/query/src/__tests__/init-utility.test.ts b/foundations/core/packages/query/src/__tests__/init-utility.test.ts new file mode 100644 index 0000000000..58bfa9f6e5 --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/init-utility.test.ts @@ -0,0 +1,452 @@ +// Tests for initialization, error handling, and utility code paths +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import core, { createClient, Ref, TxOperations } from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' +import { test } from './minmodel' + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx) => { + void liveQuery.tx(...tx) + } + return { + liveQuery, + factory: new TxOperations(storage, core.account.System), + close: async () => { + await liveQuery.close() + } + } +} + +describe('Initialization and Utility Code', () => { + describe('refreshConnect with clean flag', () => { + it('should clean queries when refreshConnect is called with clean=true', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'clean-refresh-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'test' + }) + + const callback = jest.fn() + + // Create query + liveQuery.query(test.class.TestComment, {}, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Call refreshConnect with clean=true + await liveQuery.refreshConnect(true) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Should have been called with empty result during clean, then refreshed + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + + it('should handle queued queries during refreshConnect with clean', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'queue-clean-refresh', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Create and unsubscribe to move to queue + const unsubscribe = liveQuery.query(test.class.TestComment, { space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + unsubscribe() + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Clean refresh + await liveQuery.refreshConnect(true) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should remove queries with no callbacks during refreshConnect', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'remove-no-callbacks', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Create query + const unsubscribe = liveQuery.query(test.class.TestComment, { space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Unsubscribe to remove all callbacks + unsubscribe() + + await new Promise((resolve) => setTimeout(resolve, 50)) + + // Now refresh - queries with no callbacks should be removed + await liveQuery.refreshConnect(false) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Query should still work + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('findOne with projection', () => { + it('should add required fields to projection in findOne', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'projection-test', + description: 'test description', + private: false, + members: [], + archived: false + }) + + // findOne with projection - should auto-add _class, space, modifiedOn + const result = await liveQuery.findOne( + core.class.Space, + { _id: space }, + { + projection: { + name: 1 + } + } + ) + + expect(result).toBeDefined() + expect(result?.name).toBe('projection-test') + expect(result?._class).toBeDefined() + expect(result?.space).toBeDefined() + + await close() + }) + + it('should handle findOne with empty options (auto-create options)', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'no-options-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + // findOne without options - should create options with limit=1 + const result = await liveQuery.findOne(core.class.Space, { _id: space }) + + expect(result).toBeDefined() + expect(result?.name).toBe('no-options-test') + + await close() + }) + + it('should handle findOne from refs cache', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'refs-cache-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + // First call - populates refs cache + const result1 = await liveQuery.findOne(core.class.Space, { _id: space }, { limit: 1 }) + + expect(result1).toBeDefined() + + // Second call - should come from refs cache + const result2 = await liveQuery.findOne(core.class.Space, { _id: space }, { limit: 1 }) + + expect(result2).toBeDefined() + expect(result2?._id).toBe(space) + + await close() + }) + }) + + describe('findOne with queued queries', () => { + it('should move query from queue back to active queries on findOne', async () => { + const { liveQuery, factory, close } = await getClient() + + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'queue-to-active', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Create query then unsubscribe to move to queue + const unsubscribe = liveQuery.query(core.class.Space, { name: 'queue-to-active' }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + unsubscribe() + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Now call findOne with same query - should find it in queue + const result = await liveQuery.findOne(core.class.Space, { name: 'queue-to-active' }) + + expect(result).toBeDefined() + expect(result?.name).toBe('queue-to-active') + + await close() + }) + }) + + describe('Query result promise handling', () => { + it('should await query result if it is a promise', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'promise-result', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Create a query that will have a promise result initially + const result = await liveQuery.findOne(core.class.Space, { _id: space }) + + expect(result).toBeDefined() + expect(result?.name).toBe('promise-result') + + await close() + }) + }) + + describe('Query cleanup', () => { + it('should clean query result when cleanQuery is called', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'clean-query', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Refresh with clean will call cleanQuery + await liveQuery.refreshConnect(true) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Should have received empty result during clean, then new results + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Multiple simultaneous operations', () => { + it('should handle multiple findOne calls in parallel', async () => { + const { liveQuery, factory, close } = await getClient() + + const spaces: Array> = [] + for (let i = 0; i < 5; i++) { + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: `parallel-${i}`, + description: 'test', + private: false, + members: [], + archived: false + }) + spaces.push(space) + } + + // Call findOne for all spaces in parallel + const results = await Promise.all(spaces.map((space) => liveQuery.findOne(core.class.Space, { _id: space }))) + + expect(results).toHaveLength(5) + results.forEach((result, idx) => { + expect(result).toBeDefined() + expect(result?.name).toBe(`parallel-${idx}`) + }) + + await close() + }) + + it('should handle multiple query subscriptions in parallel', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'parallel-queries', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callbacks = [jest.fn(), jest.fn(), jest.fn()] + + // Create multiple subscriptions in parallel + callbacks.forEach((callback) => { + liveQuery.query(core.class.Space, { _id: space }, callback) + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // All callbacks should have been called + callbacks.forEach((callback) => { + expect(callback).toHaveBeenCalled() + }) + + await close() + }) + }) + + describe('Edge case: Empty findAll result', () => { + it('should handle findAll returning empty results', async () => { + const { liveQuery, factory, close } = await getClient() + + // Just make sure liveQuery is initialized + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'dummy', + description: 'test', + private: false, + members: [], + archived: false + }) + + const result = await liveQuery.findAll(test.class.TestComment, { message: 'does-not-exist-ever' }) + + expect(result).toHaveLength(0) + + await close() + }) + + it('should handle findOne returning undefined', async () => { + const { liveQuery, factory, close } = await getClient() + + // Just make sure liveQuery is initialized + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'dummy', + description: 'test', + private: false, + members: [], + archived: false + }) + + const result = await liveQuery.findOne(test.class.TestComment, { message: 'does-not-exist-ever' }) + + expect(result).toBeUndefined() + + await close() + }) + }) + + describe('Query with limit and projection', () => { + it('should handle query with both limit and projection', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'limit-projection', + description: 'test description', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { _id: space }, callback, { + limit: 10, + projection: { + name: 1, + description: 1 + } + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] + expect(result.name).toBe('limit-projection') + expect(result._class).toBeDefined() + + await close() + }) + }) + + describe('Result clone operations', () => { + it('should return cloned result from findOne', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'clone-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const result = await liveQuery.findOne(core.class.Space, { _id: space }) + + expect(result).toBeDefined() + + // Modify the result - should not affect internal state + if (result !== undefined) { + ;(result as any).name = 'modified' + } + + // Query again - should get original value + const result2 = await liveQuery.findOne(core.class.Space, { _id: space }) + + expect(result2?.name).toBe('clone-test') + + await close() + }) + }) +}) diff --git a/foundations/core/packages/query/src/__tests__/livequery-coverage.test.ts b/foundations/core/packages/query/src/__tests__/livequery-coverage.test.ts new file mode 100644 index 0000000000..0d71440532 --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/livequery-coverage.test.ts @@ -0,0 +1,379 @@ +// Coverage improvement tests for LiveQuery functionality +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import core, { createClient, TxOperations } from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx) => { + void liveQuery.tx(...tx) + } + return { + liveQuery, + factory: new TxOperations(storage, core.account.System), + close: async () => { + await liveQuery.close() + } + } +} + +describe('LiveQuery Coverage Tests', () => { + it('should handle refreshConnect with clean=true', async () => { + const { liveQuery, close } = await getClient() + + const callback = jest.fn() + + // Create a query + const unsubscribe = liveQuery.query(core.class.Space, {}, callback) + + // Wait for initial results + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + callback.mockClear() + + // Unsubscribe to move to queue + unsubscribe() + await new Promise((resolve) => setTimeout(resolve, 50)) + + // Refresh with clean=true should reset the query + await liveQuery.refreshConnect(true) + + // Verify the query was cleaned + await close() + }) + + it('should handle isClosed', async () => { + const { liveQuery } = await getClient() + + expect(liveQuery.isClosed()).toBe(false) + + await liveQuery.close() + + expect(liveQuery.isClosed()).toBe(true) + }) + + it('should handle findOne with projection', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create a document + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'test-space', + description: 'test', + private: false, + members: [], + archived: false + }) + + // FindOne with projection + const result = await liveQuery.findOne(core.class.Space, { _id: space }, { projection: { name: 1 } }) + + expect(result).toBeDefined() + expect(result?.name).toBe('test-space') + + await close() + }) + + it('should pass searchFulltext to client', async () => { + const { liveQuery, close } = await getClient() + + const result = await liveQuery.searchFulltext({ query: 'test' }, {}) + + expect(result).toBeDefined() + + await close() + }) + + it('should return hierarchy and model from client', async () => { + const { liveQuery, close } = await getClient() + + const hierarchy = liveQuery.getHierarchy() + const model = liveQuery.getModel() + + expect(hierarchy).toBeDefined() + expect(model).toBeDefined() + + await close() + }) + + it('should handle multiple callbacks for same query', async () => { + const { liveQuery, close } = await getClient() + + const callback1 = jest.fn() + const callback2 = jest.fn() + const callback3 = jest.fn() + + liveQuery.query(core.class.Space, {}, callback1) + liveQuery.query(core.class.Space, {}, callback2) + liveQuery.query(core.class.Space, {}, callback3) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // All callbacks should be registered and called + expect(callback1).toHaveBeenCalled() + expect(callback2).toHaveBeenCalled() + expect(callback3).toHaveBeenCalled() + + await close() + }) + + it('should remove only specific callback on unsubscribe', async () => { + const { liveQuery, close } = await getClient() + + const callback1 = jest.fn() + const callback2 = jest.fn() + + liveQuery.query(core.class.Space, {}, callback1) + const unsubscribe2 = liveQuery.query(core.class.Space, {}, callback2) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + callback1.mockClear() + callback2.mockClear() + + unsubscribe2() + + // callback1 should still be active + await new Promise((resolve) => setTimeout(resolve, 50)) + + await close() + }) + + it('should handle query moving from active to queue and back', async () => { + const { liveQuery, close } = await getClient() + + const callback1 = jest.fn() + const callback2 = jest.fn() + + // Subscribe + const unsubscribe1 = liveQuery.query(core.class.Space, {}, callback1) + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback1).toHaveBeenCalled() + + // Unsubscribe - moves to queue + unsubscribe1() + await new Promise((resolve) => setTimeout(resolve, 50)) + + callback1.mockClear() + callback2.mockClear() + + // Subscribe again with same query - should reuse from queue + liveQuery.query(core.class.Space, {}, callback2) + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback2).toHaveBeenCalled() + + await close() + }) + + it('should handle TxUpdateDoc with search query', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create a document + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'searchable-space', + description: 'test document for search', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Query with search (though actual search won't work in mock, it tests the code path) + liveQuery.query(core.class.Space, { _id: space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + // Update the document + await factory.updateDoc(core.class.Space, core.space.Model, space, { + name: 'updated-space' + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Callback should have been called again with updated results + expect(callback.mock.calls.length).toBeGreaterThan(1) + + await close() + }) + + it('should handle TxRemoveDoc with total tracking', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create a document + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'to-be-removed', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Query with total tracking + liveQuery.query(core.class.Space, { _id: space }, callback, { total: true }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + callback.mockClear() + + // Remove the document + await factory.removeDoc(core.class.Space, core.space.Model, space) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Callback should have been called with updated results + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle lookup queries', async () => { + const { liveQuery, close } = await getClient() + + const callback = jest.fn() + + // Query with reverse lookup + liveQuery.query(core.class.Space, {}, callback, { + lookup: { + _id: { + attachedDocs: core.class.Doc + } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should compare options correctly ignoring ctx', async () => { + const { liveQuery, close } = await getClient() + + const callback1 = jest.fn() + const callback2 = jest.fn() + + // Query with same options (should reuse) + liveQuery.query(core.class.Space, {}, callback1, { limit: 10, sort: { modifiedOn: 1 } }) + + liveQuery.query(core.class.Space, {}, callback2, { limit: 10, sort: { modifiedOn: 1 } }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback1).toHaveBeenCalled() + expect(callback2).toHaveBeenCalled() + + await close() + }) + + it('should treat different options as different queries', async () => { + const { liveQuery, close } = await getClient() + + const callback1 = jest.fn() + const callback2 = jest.fn() + + liveQuery.query(core.class.Space, {}, callback1, { limit: 10 }) + + liveQuery.query(core.class.Space, {}, callback2, { limit: 20 }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback1).toHaveBeenCalled() + expect(callback2).toHaveBeenCalled() + + await close() + }) + + it('should handle TxMixin updates', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create a space document + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'mixin-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { _id: space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle document creation via transaction', async () => { + const { liveQuery, factory, close } = await getClient() + + const callback = jest.fn() + + // Set up query before creating document + liveQuery.query(core.class.Space, { name: 'new-space' }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCallCount = callback.mock.calls.length + + // Create matching document + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'new-space', + description: 'newly created', + private: false, + members: [], + archived: false + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Should have been called again with new document + expect(callback.mock.calls.length).toBeGreaterThan(initialCallCount) + + await close() + }) + + it('should handle refreshConnect without clean', async () => { + const { liveQuery, close } = await getClient() + + const callback = jest.fn() + + liveQuery.query(core.class.Space, {}, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Refresh without clean + await liveQuery.refreshConnect(false) + + await close() + }) + + it('should handle associations option', async () => { + const { liveQuery, close } = await getClient() + + const callback = jest.fn() + + liveQuery.query(core.class.Space, {}, callback, { associations: [] }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) +}) diff --git a/foundations/core/packages/query/src/__tests__/minmodel.ts b/foundations/core/packages/query/src/__tests__/minmodel.ts new file mode 100644 index 0000000000..6cb584cc6e --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/minmodel.ts @@ -0,0 +1,269 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { + PersonId, + Arr, + Class, + Data, + Doc, + Domain, + Mixin, + Obj, + Ref, + Space, + TxCreateDoc, + TxCUD, + AccountUuid +} from '@hcengineering/core' +import core, { AttachedDoc, ClassifierKind, DOMAIN_MODEL, DOMAIN_TX, TxFactory } from '@hcengineering/core' +import type { IntlString, Plugin } from '@hcengineering/platform' +import { plugin } from '@hcengineering/platform' + +const txFactory = new TxFactory(core.account.System) + +function createClass (_class: Ref>, attributes: Data>): TxCreateDoc { + return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class) +} + +/** + * @public + */ +export function createDoc ( + _class: Ref>, + attributes: Data, + id?: Ref, + modifiedBy?: PersonId +): TxCreateDoc { + const result = txFactory.createTxCreateDoc(_class, core.space.Model, attributes, id) + if (modifiedBy !== undefined) { + result.modifiedBy = modifiedBy + } + return result +} + +/** + * @public + */ +export interface TestMixin extends Doc { + arr: Arr +} + +/** + * @public + */ +export interface AttachedComment extends AttachedDoc { + message: string +} + +interface TestProject extends Space { + prjName: string +} + +interface TestProjectMixin extends TestProject { + someField?: string +} + +/** + * @public + */ +export const test = plugin('test' as Plugin, { + mixin: { + TestMixin: '' as Ref>, + TestProjectMixin: '' as Ref> + }, + class: { + TestComment: '' as Ref>, + ParticipantsHolder: '' as Ref>, + TestProject: '' as Ref> + } +}) + +/** + * @public + */ +export interface ParticipantsHolder extends Doc { + participants?: Ref[] +} + +const DOMAIN_TEST: Domain = 'test' as Domain + +/** + * @public + * Generate minimal model for testing purposes. + * @returns R + */ +export function genMinModel (): TxCUD[] { + const txes = [] + // Fill Tx'es with basic model classes. + txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS })) + txes.push( + createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS }) + ) + txes.push( + createClass(core.class.Class, { + label: 'Class' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + txes.push( + createClass(core.class.Mixin, { + label: 'Mixin' as IntlString, + extends: core.class.Class, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + txes.push( + createClass(core.class.AttachedDoc, { + label: 'AttachedDoc' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.MIXIN + }) + ) + txes.push( + createClass(core.class.Space, { + label: 'Space' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + // TODO: fixme! + // txes.push( + // createClass(core.class.Account, { + // label: 'Account' as IntlString, + // extends: core.class.Doc, + // kind: ClassifierKind.CLASS, + // domain: DOMAIN_MODEL + // }) + // ) + + txes.push( + createClass(core.class.Tx, { + label: 'Tx' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TX + }) + ) + txes.push( + createClass(core.class.TxCUD, { + label: 'TxCUD' as IntlString, + extends: core.class.Tx, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TX + }) + ) + txes.push( + createClass(core.class.TxCreateDoc, { + label: 'TxCreateDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxUpdateDoc, { + label: 'TxUpdateDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxRemoveDoc, { + label: 'TxRemoveDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxMixin, { + label: 'TxMixin' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + + txes.push( + createClass(test.mixin.TestMixin, { + label: 'TestMixin' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.MIXIN + }) + ) + + txes.push( + createClass(test.class.TestProject, { + label: 'TestProject' as IntlString, + extends: core.class.Space, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TEST + }) + ) + + txes.push( + createClass(test.mixin.TestProjectMixin, { + label: 'TestProjectMixin' as IntlString, + extends: test.class.TestProject, + kind: ClassifierKind.MIXIN + }) + ) + + txes.push( + createClass(test.class.TestComment, { + label: 'TestComment' as IntlString, + extends: core.class.AttachedDoc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TEST + }) + ) + + txes.push( + createClass(test.class.ParticipantsHolder, { + label: 'ParticipantsHolder' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TEST + }) + ) + + const u1 = 'User1' as AccountUuid + const u2 = 'User2' as AccountUuid + // TODO: fixme! + txes.push( + // createDoc(core.class.Account, { email: 'user1@site.com', role: AccountRole.User }, u1), + // createDoc(core.class.Account, { email: 'user2@site.com', role: AccountRole.User }, u2), + createDoc(core.class.Space, { + name: 'Sp1', + description: '', + private: false, + members: [u1, u2], + archived: false + }) + ) + + txes.push( + createDoc(core.class.Space, { + name: 'Sp2', + description: '', + private: false, + members: [u1], + archived: false + }) + ) + return txes +} diff --git a/foundations/core/packages/query/src/__tests__/push-pull.test.ts b/foundations/core/packages/query/src/__tests__/push-pull.test.ts new file mode 100644 index 0000000000..a0ee4cb042 --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/push-pull.test.ts @@ -0,0 +1,546 @@ +// Tests for $push/$pull operations in lookups +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import core, { createClient, Ref, TxOperations } from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' +import { test } from './minmodel' + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx) => { + void liveQuery.tx(...tx) + } + return { + liveQuery, + factory: new TxOperations(storage, core.account.System), + close: async () => { + await liveQuery.close() + } + } +} + +describe('Push/Pull Operations with Lookups', () => { + describe('$push with array lookup values', () => { + it('should update lookup when $push is used with array of IDs', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'push-array-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const callback = jest.fn() + + // Query with reverse lookup + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Create multiple child comments + await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { + message: 'child1' + }) + + await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { + message: 'child2' + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + const lastResult = callback.mock.calls[callback.mock.calls.length - 1][0][0] + expect(lastResult.$lookup).toBeDefined() + + await close() + }) + + it('should handle $push with single ID value in lookup', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'push-single-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Add a single child + await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { + message: 'single child' + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + }) + + describe('$pull with array lookup values', () => { + it('should update lookup when $pull is used with array of IDs', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'pull-array-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + // Create multiple children + const children: Array> = [] + for (let i = 0; i < 3; i++) { + const child = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: `child-${i}` + } + ) + children.push(child) + } + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Remove multiple children + await factory.removeCollection( + test.class.TestComment, + space, + children[0], + parentComment, + test.class.TestComment, + 'comments' + ) + + await factory.removeCollection( + test.class.TestComment, + space, + children[1], + parentComment, + test.class.TestComment, + 'comments' + ) + + await new Promise((resolve) => setTimeout(resolve, 200)) + + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + + it('should handle $pull with single ID value in lookup', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'pull-single-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const childComment = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'single child' + } + ) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Remove single child + await factory.removeCollection( + test.class.TestComment, + space, + childComment, + parentComment, + test.class.TestComment, + 'comments' + ) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + }) + + describe('Lookup initialization with undefined $lookup', () => { + it('should initialize $lookup array when undefined on $push', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'init-push-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent with no children initially' + } + ) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Add first child - should initialize $lookup.comments array + await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { + message: 'first child' + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should initialize $lookup array when undefined on $pull', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'init-pull-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const childComment = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'child' + } + ) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Remove child - should handle undefined $lookup gracefully + await factory.removeCollection( + test.class.TestComment, + space, + childComment, + parentComment, + test.class.TestComment, + 'comments' + ) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + }) + + describe('Complex lookup scenarios', () => { + it('should handle nested lookups with $push operations', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'nested-push-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const rootComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'root' + } + ) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: rootComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment }, + space: core.class.Space + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Add nested comment + await factory.addCollection(test.class.TestComment, space, rootComment, test.class.TestComment, 'comments', { + message: 'nested' + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle multiple simultaneous $push operations', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'multi-push-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Add multiple children rapidly + await Promise.all([ + factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { + message: 'child-a' + }), + factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { + message: 'child-b' + }), + factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { + message: 'child-c' + }) + ]) + + await new Promise((resolve) => setTimeout(resolve, 200)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle mixed $push and $pull operations', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'mixed-ops-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const existingChild = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'existing' + } + ) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Add new child + await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { + message: 'new' + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Remove existing child + await factory.removeCollection( + test.class.TestComment, + space, + existingChild, + parentComment, + test.class.TestComment, + 'comments' + ) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) +}) diff --git a/foundations/core/packages/query/src/__tests__/query.test.ts b/foundations/core/packages/query/src/__tests__/query.test.ts new file mode 100644 index 0000000000..bd6ea9526c --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/query.test.ts @@ -0,0 +1,1023 @@ +// +// Copyright © 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { + createClient, + Doc, + generateId, + MeasureMetricsContext, + Ref, + SortingOrder, + Space, + systemAccountUuid, + Tx, + TxCreateDoc, + TxOperations, + WithLookup +} from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' +import { AttachedComment, genMinModel, ParticipantsHolder, test } from './minmodel' + +interface Channel extends Space { + x: number +} + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx: Tx[]) => { + liveQuery.tx(...tx).catch((err) => { + console.log(err) + }) + } + return { liveQuery, factory: new TxOperations(storage, core.account.System) } +} + +describe('query', () => { + it('findAll', async () => { + const { liveQuery } = await getClient() + const result = await liveQuery.findAll(core.class.Space, {}) + expect(result).toHaveLength(2) + }) + + it('query with param', async () => { + const { liveQuery } = await getClient() + + let expectedLength = 0 + const txes = genMinModel() + for (let i = 0; i < txes.length; i++) { + if (liveQuery.getHierarchy().isDerived((txes[i] as TxCreateDoc).objectClass, core.class.Space)) { + expectedLength++ + } + } + + const result = await new Promise((resolve) => { + liveQuery.query(core.class.Space, { private: false }, (result) => { + resolve(result) + }) + }) + expect(result).toHaveLength(expectedLength) + }) + + it('query should be live', async () => { + const { liveQuery, factory } = await getClient() + + let expectedLength = 0 + const txes = genMinModel() + for (let i = 0; i < txes.length; i++) { + if (liveQuery.getHierarchy().isDerived((txes[i] as TxCreateDoc).objectClass, core.class.Space)) { + expectedLength++ + } + } + + let attempt = 0 + const pp = new Promise((resolve) => { + liveQuery.query(core.class.Space, { private: false }, (result) => { + expect(result).toHaveLength(expectedLength + attempt) + if (attempt > 0) { + expect((result[expectedLength + attempt - 1] as any).x).toBe(attempt) + } + if (attempt++ === 3) { + // check underlying storage received all data. + liveQuery + .findAll(core.class.Space, { private: false }) + .then((result) => { + expect(result).toHaveLength(expectedLength + attempt - 1) + resolve(null) + }) + .catch((err) => { + expect(err).toBeUndefined() + }) + } + }) + }) + + // TODO: fixme! + // await factory.createDoc(core.class.Account, core.space.Model, { + // email: 'user1@site.com', + // role: AccountRole.User + // }) + await factory.createDoc(core.class.Space, core.space.Model, { + private: true, + name: '#0', + description: '', + members: [], + archived: false, + x: 0 + }) + await factory.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#1', + description: '', + members: [], + archived: false, + x: 1 + }) + await factory.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#2', + description: '', + members: [], + archived: false, + x: 2 + }) + await factory.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#3', + description: '', + members: [], + archived: false, + x: 3 + }) + await pp + }) + + it('unsubscribe query', async () => { + const { liveQuery, factory } = await getClient() + + let expectedLength = 0 + const txes = genMinModel() + for (let i = 0; i < txes.length; i++) { + if (liveQuery.getHierarchy().isDerived((txes[i] as TxCreateDoc).objectClass, core.class.Space)) { + expectedLength++ + } + } + + const unsubscribe = liveQuery.query(core.class.Space, { private: false }, (result) => { + expect(result).toHaveLength(expectedLength) + }) + + unsubscribe() + + await factory.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#1', + description: '', + archived: false, + members: [] + }) + await factory.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#2', + description: '', + archived: false, + members: [] + }) + await factory.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#3', + description: '', + archived: false, + members: [] + }) + }) + + it('query against core client', async () => { + const { liveQuery, factory } = await getClient() + + const expectedLength = 2 + let attempt = 0 + const pp = new Promise((resolve) => { + liveQuery.query(core.class.Space, { private: false }, (result) => { + expect(result).toHaveLength(expectedLength + attempt) + if (attempt > 0) { + expect((result[expectedLength + attempt - 1] as any).x).toBe(attempt) + } + if (attempt++ === 1) resolve(null) + }) + }) + + await factory.createDoc(core.class.Space, core.space.Model, { + x: 1, + private: false, + name: '#1', + description: '', + archived: false, + members: [] + }) + await factory.createDoc(core.class.Space, core.space.Model, { + x: 2, + private: false, + name: '#2', + description: '', + archived: false, + members: [] + }) + await factory.createDoc(core.class.Space, core.space.Model, { + x: 3, + private: false, + name: '#3', + description: '', + archived: false, + members: [] + }) + await pp + }) + + it('limit and sorting', async () => { + const { liveQuery, factory } = await getClient() + + const limit = 1 + let attempt = 0 + let descAttempt = 0 + + const pp1 = new Promise((resolve) => { + liveQuery.query( + core.class.Space, + { private: true }, + (result) => { + if (result.length > 0) { + expect(result.length).toEqual(limit) + expect(result[0].name).toMatch('0') + attempt++ + } + if (attempt === 1) resolve(null) + }, + { limit, sort: { name: SortingOrder.Ascending } } + ) + }) + + const pp2 = new Promise((resolve) => { + liveQuery.query( + core.class.Space, + { private: true }, + (result) => { + if (result.length > 0) { + expect(result.length).toEqual(limit) + expect(result[0].name).toMatch(descAttempt.toString()) + descAttempt++ + } + if (descAttempt === 10) resolve(null) + }, + { limit, sort: { name: SortingOrder.Descending } } + ) + }) + + for (let i = 0; i < 10; i++) { + await factory.createDoc(core.class.Space, core.space.Model, { + private: true, + name: i.toString(), + description: '', + archived: false, + members: [] + }) + } + await Promise.all([pp1, pp2]) + }) + + it('remove', async () => { + const { liveQuery, factory } = await getClient() + + const expectedLength = 2 + let attempt = 0 + let x: undefined | ((s: any) => void) + const y = new Promise((resolve) => (x = resolve)) + + const pp = new Promise((resolve) => { + liveQuery.query(core.class.Space, { private: false }, (result) => { + expect(result).toHaveLength(expectedLength - attempt) + if (attempt === 0) x?.(null) + if (attempt++ === expectedLength) resolve(null) + }) + }) + await y + const spaces = await liveQuery.findAll(core.class.Space, {}) + for (const space of spaces) { + await factory.removeDoc(space._class, space.space, space._id) + } + await pp + }) + + it('remove with limit', async () => { + const { liveQuery, factory } = await getClient() + + const expectedLength = 2 + let attempt = 0 + let x: undefined | ((s: any) => void) + const y = new Promise((resolve) => (x = resolve)) + const pp = new Promise((resolve) => { + liveQuery.query( + core.class.Space, + { private: false }, + (result) => { + if (attempt === 0) x?.(null) + expect(result).toHaveLength(attempt++ === expectedLength ? 0 : 1) + if (attempt === expectedLength) resolve(null) + }, + { limit: 1 } + ) + }) + + await y + const spaces = await liveQuery.findAll(core.class.Space, {}) + for (const space of spaces) { + await factory.removeDoc(space._class, space.space, space._id) + } + await pp + }) + + it('update', async () => { + const { liveQuery, factory } = await getClient() + + const spaces = await liveQuery.findAll(core.class.Space, {}) + let attempt = 0 + const pp = new Promise((resolve) => { + liveQuery.query( + core.class.Space, + { private: false }, + (result) => { + if (attempt > 0) { + expect(result[attempt - 1].name === attempt.toString()) + expect(result[attempt - 1].members.length === 1) + if (attempt === spaces.length) resolve(null) + } + }, + { sort: { private: SortingOrder.Ascending } } + ) + }) + + for (const space of spaces) { + attempt++ + await factory.updateDoc(space._class, space.space, space._id, { + name: attempt.toString(), + $push: { members: systemAccountUuid } + }) + } + await pp + }) + + it('update with no match query', async () => { + const { liveQuery, factory } = await getClient() + + const spaces = await liveQuery.findAll(core.class.Space, {}) + let attempt = 0 + const pp = new Promise((resolve) => { + liveQuery.query( + core.class.Space, + { private: false }, + (result) => { + if (attempt > 0) { + expect(result.length === spaces.length - attempt) + if (attempt === spaces.length) resolve(null) + } + }, + { sort: { private: SortingOrder.Ascending } } + ) + }) + + for (const space of spaces) { + attempt++ + await factory.updateDoc(space._class, space.space, space._id, { + private: true + }) + } + await pp + }) + + it('lookup query add doc', async () => { + const { liveQuery, factory } = await getClient() + const futureSpace: Space = { + _id: generateId(), + _class: core.class.Space, + private: false, + members: [], + space: core.space.Model, + name: 'new space', + description: '', + archived: false, + modifiedBy: core.account.System, + modifiedOn: 0 + } + const comment = await factory.addCollection( + test.class.TestComment, + futureSpace._id, + futureSpace._id, + core.class.Space, + 'comments', + { + message: 'test' + } + ) + let attempt = 0 + const pp = new Promise((resolve) => { + liveQuery.query( + test.class.TestComment, + { _id: comment }, + (result) => { + const comment = result[0] + if (comment !== undefined) { + if (attempt > 0) { + expect(comment.$lookup?.space?._id).toEqual(futureSpace._id) + resolve(null) + } else { + expect(comment.$lookup?.space).toBeUndefined() + attempt++ + void factory.createDoc( + core.class.Space, + futureSpace.space, + { + ...futureSpace + }, + futureSpace._id + ) + } + } + }, + { lookup: { space: core.class.Space } } + ) + }) + + await pp + }) + + it('lookup nested query add doc', async () => { + const { liveQuery, factory } = await getClient() + const futureSpace: Space = { + _id: generateId(), + _class: core.class.Space, + private: false, + members: [], + space: core.space.Model, + name: 'new space', + description: '', + archived: false, + modifiedBy: core.account.System, + modifiedOn: 0 + } + const comment = await factory.addCollection( + test.class.TestComment, + futureSpace._id, + futureSpace._id, + core.class.Space, + 'comments', + { + message: 'test' + } + ) + const childComment = await factory.addCollection( + test.class.TestComment, + futureSpace._id, + comment, + test.class.TestComment, + 'comments', + { + message: 'child' + } + ) + const pp = new Promise((resolve) => { + liveQuery.query( + test.class.TestComment, + { _id: childComment }, + (result) => { + const comment = result[0] + if (comment !== undefined) { + expect((comment.$lookup?.attachedTo as WithLookup)?.$lookup?.space?._id).toEqual( + futureSpace._id + ) + resolve(null) + } + }, + { lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } } + ) + }) + + await factory.createDoc( + core.class.Space, + futureSpace.space, + { + ...futureSpace + }, + futureSpace._id + ) + await pp + }) + + it('lookup reverse query add doc', async () => { + const { liveQuery, factory } = await getClient() + const spaces = await liveQuery.findAll(core.class.Space, {}) + const parentComment = await factory.addCollection( + test.class.TestComment, + spaces[0]._id, + spaces[0]._id, + spaces[0]._class, + 'comments', + { + message: 'test' + } + ) + const childLength = 3 + const pp = new Promise((resolve) => { + liveQuery.query( + test.class.TestComment, + { _id: parentComment }, + (result) => { + const comment = result[0] + const res = (comment.$lookup as any)?.comments?.length + + if (res !== undefined) { + expect(res).toBeGreaterThanOrEqual(1) + expect(res).toBeLessThanOrEqual(childLength) + } + if ((res ?? 0) === childLength) { + resolve(null) + } + }, + { lookup: { _id: { comments: test.class.TestComment } } } + ) + }) + + for (let index = 0; index < childLength; index++) { + await factory.addCollection( + test.class.TestComment, + spaces[0]._id, + parentComment, + test.class.TestComment, + 'comments', + { + message: index.toString() + } + ) + } + await pp + }) + + it('lookup query remove doc', async () => { + const { liveQuery, factory } = await getClient() + const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'new space', + description: '', + archived: false, + private: false, + members: [] + }) + const comment = await factory.addCollection( + test.class.TestComment, + futureSpace, + futureSpace, + core.class.Space, + 'comments', + { + message: 'test' + } + ) + let attempt = 0 + const pp = new Promise((resolve) => { + liveQuery.query( + test.class.TestComment, + { _id: comment }, + (result) => { + const comment = result[0] + if (comment !== undefined) { + if (attempt > 0) { + expect(comment.$lookup?.space).toBeUndefined() + resolve(null) + } else { + expect((comment.$lookup?.space as Doc)?._id).toEqual(futureSpace) + attempt++ + void factory.removeDoc(core.class.Space, core.space.Model, futureSpace) + } + } + }, + { lookup: { space: core.class.Space } } + ) + }) + + await pp + }) + + it('lookup nested query remove doc', async () => { + const { liveQuery, factory } = await getClient() + const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'new space', + description: '', + archived: false, + private: false, + members: [] + }) + const comment = await factory.addCollection( + test.class.TestComment, + futureSpace, + futureSpace, + core.class.Space, + 'comments', + { + message: 'test' + } + ) + const childComment = await factory.addCollection( + test.class.TestComment, + futureSpace, + comment, + test.class.TestComment, + 'comments', + { + message: 'child' + } + ) + const pp = new Promise((resolve) => { + liveQuery.query( + test.class.TestComment, + { _id: childComment }, + (result) => { + const comment = result[0] + if (comment !== undefined) { + expect((comment.$lookup?.attachedTo as WithLookup)?.$lookup?.space).toBeUndefined() + resolve(null) + } + }, + { lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } } + ) + }) + + await factory.removeDoc(core.class.Space, core.space.Model, futureSpace) + + await pp + }) + + it('lookup reverse query remove doc', async () => { + const { liveQuery, factory } = await getClient() + const spaces = await liveQuery.findAll(core.class.Space, {}) + const comments = await liveQuery.findAll(test.class.TestComment, {}) + expect(comments).toHaveLength(0) + const parentComment = await factory.addCollection( + test.class.TestComment, + spaces[0]._id, + spaces[0]._id, + spaces[0]._class, + 'comments', + { + message: 'test' + } + ) + let attempt = -1 + const childLength = 3 + const childs: Ref[] = [] + for (let index = 0; index < childLength; index++) { + childs.push( + await factory.addCollection( + test.class.TestComment, + spaces[0]._id, + parentComment, + test.class.TestComment, + 'comments', + { + message: index.toString() + } + ) + ) + } + + let secondPromise: Promise | undefined + const firstCallback = new Promise((resolve) => { + secondPromise = new Promise((_resolve) => { + liveQuery.query( + test.class.TestComment, + { _id: parentComment }, + (result) => { + attempt++ + if (attempt === 0) { + resolve() + } + const comment = result[0] + if (comment !== undefined) { + expect((comment.$lookup as any)?.comments).toHaveLength(childLength - attempt) + } + if (attempt === childLength) { + _resolve() + } + }, + { lookup: { _id: { comments: test.class.TestComment } } } + ) + }) + }) + + await firstCallback + + for (const child of childs) { + await factory.removeCollection( + test.class.TestComment, + spaces[0]._id, + child, + parentComment, + test.class.TestComment, + 'comments' + ) + } + await secondPromise + }) + + it('lookup query update doc', async () => { + const { liveQuery, factory } = await getClient() + let attempt = 0 + const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, { + name: '0', + description: '', + archived: false, + private: false, + members: [] + }) + + const comment = await factory.addCollection( + test.class.TestComment, + futureSpace, + futureSpace, + core.class.Space, + 'comments', + { + message: 'test' + } + ) + const pp = new Promise((resolve) => { + liveQuery.query( + test.class.TestComment, + { _id: comment }, + (result) => { + const comment = result[0] + if (comment !== undefined) { + expect((comment.$lookup?.space as Space).name).toEqual(attempt.toString()) + } + if (attempt > 0) { + resolve(null) + } else { + attempt++ + } + }, + { lookup: { space: core.class.Space } } + ) + }) + + await new Promise((resolve) => { + setTimeout(resolve, 1) + }) + + await factory.updateDoc(core.class.Space, core.space.Model, futureSpace, { + name: '1' + }) + await pp + }) + + it('lookup nested query update doc', async () => { + const { liveQuery, factory } = await getClient() + let attempt = -1 + const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, { + name: '0', + description: '', + archived: false, + private: false, + members: [] + }) + const comment = await factory.addCollection( + test.class.TestComment, + futureSpace, + futureSpace, + core.class.Space, + 'comments', + { + message: 'test' + } + ) + const childComment = await factory.addCollection( + test.class.TestComment, + futureSpace, + comment, + test.class.TestComment, + 'comments', + { + message: 'child' + } + ) + const pp = new Promise((resolve) => { + liveQuery.query( + test.class.TestComment, + { _id: childComment }, + (result) => { + attempt++ + const comment = result[0] + if (comment !== undefined) { + expect( + ((comment.$lookup?.attachedTo as WithLookup)?.$lookup?.space as Space).name + ).toEqual(attempt.toString()) + } + if (attempt > 0) { + resolve(null) + } + }, + { lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } } + ) + }) + + await factory.updateDoc(core.class.Space, core.space.Model, futureSpace, { + name: '1' + }) + await pp + }) + + it('lookup reverse query update doc', async () => { + const { liveQuery, factory } = await getClient() + const spaces = await liveQuery.findAll(core.class.Space, {}) + const parentComment = await factory.addCollection( + test.class.TestComment, + spaces[0]._id, + spaces[0]._id, + spaces[0]._class, + 'comments', + { + message: 'test' + } + ) + let attempt = -1 + const childComment = await factory.addCollection( + test.class.TestComment, + spaces[0]._id, + parentComment, + test.class.TestComment, + 'comments', + { + message: '0' + } + ) + const pp = new Promise((resolve) => { + liveQuery.query( + test.class.TestComment, + { _id: parentComment }, + (result) => { + attempt++ + const comment = result[0] + if (comment !== undefined) { + expect(((comment.$lookup as any)?.comments[0] as AttachedComment).message).toEqual(attempt.toString()) + } + if (attempt > 0) { + resolve(null) + } + }, + { lookup: { _id: { comments: test.class.TestComment } } } + ) + }) + + await factory.updateCollection( + test.class.TestComment, + spaces[0]._id, + childComment, + parentComment, + test.class.TestComment, + 'comments', + { + message: '1' + } + ) + await pp + }) + + // it('update with over limit', async () => { + // const { liveQuery, factory } = await getClient() + + // const spaces = await liveQuery.findAll(core.class.Space, {}) + // let attempt = 0 + // const pp = new Promise((resolve) => { + // liveQuery.query( + // core.class.Space, + // {}, + // (result) => { + // expect(result[0].name).toEqual(`Sp${++attempt}`) + // if (attempt === spaces.length + 1) resolve(null) + // }, + // { sort: { name: SortingOrder.Ascending }, limit: 1 } + // ) + // }) + + // for (let index = 0; index < spaces.length; index++) { + // const space = spaces[index] + // await factory.updateDoc(space._class, space.space, space._id, { + // name: `Sp${index + spaces.length + 1}` + // }) + // } + // await pp + // }) + + it('update-array-value', async () => { + const { liveQuery, factory } = await getClient() + + const spaces = await liveQuery.findAll(core.class.Space, {}) + await factory.createDoc(test.class.ParticipantsHolder, spaces[0]._id, { + participants: ['a' as Ref] + }) + const a2 = await factory.createDoc(test.class.ParticipantsHolder, spaces[0]._id, { + participants: ['b' as Ref] + }) + + const holderBefore = await liveQuery.findAll(test.class.ParticipantsHolder, { participants: 'a' as Ref }) + expect(holderBefore.length).toEqual(1) + + let attempt = 0 + let resolvePpv: (value: Doc[] | PromiseLike) => void + + const resolveP = new Promise((resolve) => { + resolvePpv = resolve + }) + const pp = await new Promise((resolve) => { + liveQuery.query( + test.class.ParticipantsHolder, + { participants: 'a' as Ref }, + (result) => { + if (attempt > 0) { + resolvePpv(result) + } else { + resolve(null) + } + }, + { sort: { private: SortingOrder.Ascending } } + ) + }) + + await pp // We have first value returned + + attempt++ + await factory.updateDoc(test.class.ParticipantsHolder, spaces[0]._id, a2, { + $push: { + participants: 'a' as Ref + } + }) + const result = await resolveP + expect(result.length).toEqual(2) + }) + + it('check query mixin projection', async () => { + const { liveQuery, factory } = await getClient() + + let projects = await liveQuery.queryFind(test.mixin.TestProjectMixin, {}, { projection: { _id: 1 } }) + expect(projects.length).toEqual(0) + const project = await factory.createDoc(test.class.TestProject, core.space.Space, { + archived: false, + description: '', + members: [], + private: false, + prjName: 'test project', + name: 'qwe' + }) + + projects = await liveQuery.queryFind(test.mixin.TestProjectMixin, {}, { projection: { _id: 1 } }) + expect(projects.length).toEqual(0) + await factory.createMixin(project, test.class.TestProject, core.space.Space, test.mixin.TestProjectMixin, { + someField: 'qwe' + }) + // We need to process all events before we could do query again + await new Promise((resolve) => { + setTimeout(resolve, 100) + }) + projects = await liveQuery.queryFind(test.mixin.TestProjectMixin, {}, { projection: { _id: 1 } }) + expect(projects.length).toEqual(1) + }) + + jest.setTimeout(25000) + it('test clone ops', async () => { + const { liveQuery, factory } = await getClient() + + const counter = 1000 + const ctx = new MeasureMetricsContext('tool', {}) + let data: Space[] = [] + const pp = new Promise((resolve) => { + liveQuery.query( + test.class.TestProject, + { private: false }, + (result) => { + data = result + if (data.length % 1000 === 0) { + console.info(data.length) + } + if (data.length === counter) { + resolve(null) + } + }, + {} + ) + }) + + for (let i = 0; i < counter; i++) { + await ctx.with('create-doc', {}, () => + factory.createDoc(test.class.TestProject, core.space.Space, { + archived: false, + description: '', + members: [], + private: false, + prjName: 'test project', + name: 'qwe' + }) + ) + } + expect(data.length).toBe(counter) + await pp + }) +}) diff --git a/foundations/core/packages/query/src/__tests__/queue-bugs.test.ts b/foundations/core/packages/query/src/__tests__/queue-bugs.test.ts new file mode 100644 index 0000000000..d40cac51ba --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/queue-bugs.test.ts @@ -0,0 +1,432 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { createClient, SortingOrder, Space, Tx, TxOperations } from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx: Tx[]) => { + liveQuery.tx(...tx).catch((err) => { + console.log(err) + }) + } + return { liveQuery, factory: new TxOperations(storage, core.account.System) } +} + +describe('LiveQuery - Queue Management Bugs', () => { + describe('Queue and Queries Map Consistency', () => { + it('should properly synchronize queue and queries map when removing queries', async () => { + const { liveQuery } = await getClient() + + const unsubscribe1 = liveQuery.query(core.class.Space, { private: false }, (result) => { + // Callback 1 + }) + + const unsubscribe2 = liveQuery.query(core.class.Space, { private: false }, (result) => { + // Callback 2 - same query + }) + + // Both callbacks should share the same query + // Let's verify internal state + const queriesMap = (liveQuery as any).queries.get(core.class.Space) + expect(queriesMap?.size).toBe(1) + + // Unsubscribe first callback + unsubscribe1() + + // Query should still exist because second callback is still active + expect(queriesMap?.size).toBe(1) + + // Unsubscribe second callback + unsubscribe2() + + // Now the query should be moved to the queue (cached for reuse) + const queue = (liveQuery as any).queue + expect(queue.size).toBeGreaterThan(0) + }) + + it('should handle rapid subscribe and unsubscribe correctly', async () => { + const { liveQuery } = await getClient() + + const callbacks: Array<() => void> = [] + + // Rapidly subscribe 10 times to the same query + for (let i = 0; i < 10; i++) { + const unsubscribe = liveQuery.query(core.class.Space, { private: false }, (result) => { + // Callback + }) + callbacks.push(unsubscribe) + } + + const queriesMap = (liveQuery as any).queries.get(core.class.Space) + // Should only have 1 query since they're all the same + expect(queriesMap?.size).toBe(1) + + // Unsubscribe all + callbacks.forEach((unsub) => { + unsub() + }) + + // Query should be in the queue + const queue = (liveQuery as any).queue + expect(queue.size).toBeGreaterThan(0) + }) + + it('should not leak memory when queries are unsubscribed', async () => { + const { liveQuery } = await getClient() + + const unsubscribeCallbacks: Array<() => void> = [] + + // Create many different queries + for (let i = 0; i < 50; i++) { + const unsubscribe = liveQuery.query(core.class.Space, { name: `query-${i}` }, (result) => { + // Callback + }) + unsubscribeCallbacks.push(unsubscribe) + } + + // All queries should be in the queries map + const queriesMap = (liveQuery as any).queries.get(core.class.Space) + expect(queriesMap?.size).toBe(50) + + // Unsubscribe all + unsubscribeCallbacks.forEach((unsub) => { + unsub() + }) + + // All queries should now be in the queue + const queue = (liveQuery as any).queue + expect(queue.size).toBe(50) + + // Wait a bit for any async operations + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Still should have 50 queries in queue + expect(queue.size).toBe(50) + }) + + it('should properly handle queue cleanup when exceeding CACHE_SIZE', async () => { + const { liveQuery } = await getClient() + + const CACHE_SIZE = 125 // From the code + const unsubscribeCallbacks: Array<() => void> = [] + + // Create more queries than CACHE_SIZE + for (let i = 0; i < CACHE_SIZE + 20; i++) { + const unsubscribe = liveQuery.query(core.class.Space, { name: `query-${i}` }, (result) => { + // Callback + }) + unsubscribeCallbacks.push(unsubscribe) + } + + const queriesMapBefore = (liveQuery as any).queries.get(core.class.Space) + expect(queriesMapBefore?.size).toBe(CACHE_SIZE + 20) + + // Unsubscribe all to move them to queue + unsubscribeCallbacks.forEach((unsub) => { + unsub() + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const queue = (liveQuery as any).queue + // Queue should not exceed CACHE_SIZE due to cleanup + expect(queue.size).toBeLessThanOrEqual(CACHE_SIZE) + + const queriesMapAfter = (liveQuery as any).queries.get(core.class.Space) + // Some queries should have been removed from the queries map too + expect(queriesMapAfter?.size).toBeLessThan(CACHE_SIZE + 20) + }) + }) + + describe('Query Callback Management', () => { + it('should handle multiple callbacks on the same query correctly', async () => { + const { liveQuery, factory } = await getClient() + + const results1: any[] = [] + const results2: any[] = [] + + const unsubscribe1 = liveQuery.query(core.class.Space, { private: false }, (result) => { + results1.push(result.length) + }) + + const unsubscribe2 = liveQuery.query(core.class.Space, { private: false }, (result) => { + results2.push(result.length) + }) + + // Wait for initial callbacks + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Both should have received the same initial data + expect(results1.length).toBeGreaterThan(0) + expect(results2.length).toBeGreaterThan(0) + expect(results1[0]).toBe(results2[0]) + + // Create a new document + await factory.createDoc(core.class.Space, core.space.Model, { + private: false, + name: 'Test Space', + description: '', + members: [], + archived: false + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Both callbacks should have been called again + expect(results1.length).toBeGreaterThan(1) + expect(results2.length).toBeGreaterThan(1) + + unsubscribe1() + unsubscribe2() + }) + + it('should stop sending updates after unsubscribe', async () => { + const { liveQuery, factory } = await getClient() + + const results: any[] = [] + + const unsubscribe = liveQuery.query(core.class.Space, { private: false }, (result) => { + results.push(result.length) + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const countBeforeUnsubscribe = results.length + + unsubscribe() + + // Create new documents after unsubscribe + for (let i = 0; i < 5; i++) { + await factory.createDoc(core.class.Space, core.space.Model, { + private: false, + name: `Space ${i}`, + description: '', + members: [], + archived: false + }) + } + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Should not have received new callbacks + expect(results.length).toBe(countBeforeUnsubscribe) + }) + }) + + describe('Query Reuse from Queue', () => { + it('should reuse cached query from queue when re-subscribing', async () => { + const { liveQuery } = await getClient() + + const unsubscribe1 = liveQuery.query(core.class.Space, { private: false }, (result) => { + // First callback + }) + + await new Promise((resolve) => setTimeout(resolve, 50)) + + const queriesMap = (liveQuery as any).queries.get(core.class.Space) + const firstQuery: any = Array.from(queriesMap.values())[0] + const initialQueryId = firstQuery?.id + + unsubscribe1() + + // Query should be in queue now + const queue = (liveQuery as any).queue + expect(queue.size).toBeGreaterThan(0) + + // Subscribe again to the same query + const unsubscribe2 = liveQuery.query(core.class.Space, { private: false }, (result) => { + // Second callback + }) + + await new Promise((resolve) => setTimeout(resolve, 50)) + + const queriesMapAfter = (liveQuery as any).queries.get(core.class.Space) + const secondQuery: any = Array.from(queriesMapAfter.values())[0] + const reusedQueryId = secondQuery?.id + + // Should reuse the same query + expect(reusedQueryId).toBe(initialQueryId) + + unsubscribe2() + }) + + it('should handle query options comparison correctly', async () => { + const { liveQuery } = await getClient() + + const unsubscribe1 = liveQuery.query(core.class.Space, { private: false }, (result) => {}, { + limit: 10, + sort: { name: SortingOrder.Ascending } + }) + + const unsubscribe2 = liveQuery.query(core.class.Space, { private: false }, (result) => {}, { + limit: 10, + sort: { name: SortingOrder.Ascending } + }) + + const queriesMap = (liveQuery as any).queries.get(core.class.Space) + // Should share the same query because options match + expect(queriesMap?.size).toBe(1) + + unsubscribe1() + unsubscribe2() + + // Different options should create different query + const unsubscribe3 = liveQuery.query(core.class.Space, { private: false }, (result) => {}, { + limit: 20, + sort: { name: SortingOrder.Descending } + }) + + await new Promise((resolve) => setTimeout(resolve, 50)) + + // Now should have 2 queries (1 in queue, 1 active) + const totalQueries = queriesMap?.size ?? 0 + ((liveQuery as any).queue.size ?? 0) + expect(totalQueries).toBeGreaterThan(1) + + unsubscribe3() + }) + }) + + describe('Edge Cases and Error Conditions', () => { + it('should handle empty query results', async () => { + const { liveQuery } = await getClient() + + const results: any[] = [] + + const unsubscribe = liveQuery.query(core.class.Space, { name: 'NonExistentSpace123456' }, (result) => { + results.push(result) + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(results.length).toBeGreaterThan(0) + expect(results[0]).toHaveLength(0) + + unsubscribe() + }) + + it('should handle query on closed LiveQuery', async () => { + const { liveQuery } = await getClient() + + await liveQuery.close() + + // Attempting to query after close should not crash + expect(() => { + liveQuery.query(core.class.Space, {}, (result) => {}) + }).not.toThrow() + }) + + it('should handle concurrent findAll and query operations', async () => { + const { liveQuery } = await getClient() + + const promises = [] + + // Start multiple findAll operations + for (let i = 0; i < 10; i++) { + promises.push(liveQuery.findAll(core.class.Space, { private: false })) + } + + // Start multiple query operations + for (let i = 0; i < 10; i++) { + liveQuery.query(core.class.Space, { private: false }, (result) => {}) + } + + const results = await Promise.all(promises) + + results.forEach((result) => { + expect(Array.isArray(result)).toBe(true) + }) + }) + }) + + describe('Query Counter and ID Management', () => { + it('should generate unique query IDs', async () => { + const { liveQuery } = await getClient() + + const queryIds = new Set() + const unsubscribes: Array<() => void> = [] + + // Create many queries + for (let i = 0; i < 100; i++) { + const unsubscribe = liveQuery.query(core.class.Space, { name: `unique-query-${i}` }, (result) => {}) + unsubscribes.push(unsubscribe) + } + + const queriesMap = (liveQuery as any).queries.get(core.class.Space) + if (queriesMap !== undefined) { + for (const query of queriesMap.values()) { + queryIds.add(query.id) + } + } + + // All IDs should be unique + expect(queryIds.size).toBe(100) + + unsubscribes.forEach((unsub) => { + unsub() + }) + }) + }) + + describe('Query Result Consistency', () => { + it('should maintain result consistency across multiple callbacks', async () => { + const { liveQuery, factory } = await getClient() + + const callback1Results: number[] = [] + const callback2Results: number[] = [] + + const unsubscribe1 = liveQuery.query(core.class.Space, { private: false }, (result) => { + callback1Results.push(result.length) + }) + + // Wait for first callback to complete + await new Promise((resolve) => setTimeout(resolve, 100)) + + const unsubscribe2 = liveQuery.query(core.class.Space, { private: false }, (result) => { + callback2Results.push(result.length) + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Create a document + await factory.createDoc(core.class.Space, core.space.Model, { + private: false, + name: 'Consistency Test', + description: '', + members: [], + archived: false + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Both callbacks should have received updates after document creation + // Note: Due to setTimeout(0) in pushCallback, timing may vary slightly + // but both should see at least 2 updates (initial + after create) + expect(callback1Results.length).toBeGreaterThanOrEqual(2) + expect(callback2Results.length).toBeGreaterThanOrEqual(2) + + // Final counts should match after everything settles + const final1 = callback1Results[callback1Results.length - 1] + const final2 = callback2Results[callback2Results.length - 1] + expect(final1).toBe(final2) + + unsubscribe1() + unsubscribe2() + }) + }) +}) diff --git a/foundations/core/packages/query/src/__tests__/refs-coverage.test.ts b/foundations/core/packages/query/src/__tests__/refs-coverage.test.ts new file mode 100644 index 0000000000..fbf27df928 --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/refs-coverage.test.ts @@ -0,0 +1,298 @@ +// Tests for Refs class to improve coverage +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import core, { createClient, TxOperations } from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx) => { + void liveQuery.tx(...tx) + } + return { + liveQuery, + factory: new TxOperations(storage, core.account.System), + close: async () => { + await liveQuery.close() + } + } +} + +describe('Refs Class Coverage Tests', () => { + it('should find document from refs cache with specific _id', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create a document + const space1 = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'cached-space-1', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Query it to cache it + const callback = jest.fn() + liveQuery.query(core.class.Space, { _id: space1 }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Now findOne should use cached version + const result = await liveQuery.findOne(core.class.Space, { _id: space1 }) + + expect(result).toBeDefined() + expect(result?._id).toBe(space1) + + await close() + }) + + it('should handle findOne with limit=1 without sort', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create multiple documents + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'test-1', + description: 'test', + private: false, + members: [], + archived: false + }) + + await factory.createDoc(core.class.Space, core.space.Model, { + name: 'test-2', + description: 'test', + private: false, + members: [], + archived: false + }) + + // findOne with limit should use refs optimization + const result = await liveQuery.findOne(core.class.Space, { private: false }, { limit: 1 }) + + expect(result).toBeDefined() + + await close() + }) + + it('should handle findOne with associations and lookup', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'lookup-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Query with lookup to populate refs cache + const callback = jest.fn() + liveQuery.query(core.class.Space, { _id: space }, callback, { lookup: { _id: { docs: core.class.Doc } } }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // FindOne should use refs cache + const result = await liveQuery.findOne( + core.class.Space, + { _id: space }, + { lookup: { _id: { docs: core.class.Doc } } } + ) + + expect(result).toBeDefined() + + await close() + }) + + it('should strip $lookup and $associations when finding from cache without them', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'strip-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Query with lookup to cache with $lookup + const callback = jest.fn() + liveQuery.query(core.class.Space, { _id: space }, callback, { lookup: { _id: { docs: core.class.Doc } } }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // FindOne without lookup should strip $lookup from cached doc + const result = await liveQuery.findOne(core.class.Space, { _id: space }) + + expect(result).toBeDefined() + expect((result as any).$lookup).toBeUndefined() + + await close() + }) + + it('should handle mixin class in findOne', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'mixin-findone', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Query to cache + const callback = jest.fn() + liveQuery.query(core.class.Space, { _id: space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Find with different class to test mixin path + const result = await liveQuery.findOne(core.class.Space, { _id: space }) + + expect(result).toBeDefined() + + await close() + }) + + it('should handle query with lookup and associations together', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'both-options', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + liveQuery.query(core.class.Space, { _id: space }, callback, { + lookup: { _id: { docs: core.class.Doc } }, + associations: [] + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle updating refs cache on document changes', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'update-refs', + description: 'original', + private: false, + members: [], + archived: false + }) + + // Cache it + const callback = jest.fn() + liveQuery.query(core.class.Space, { _id: space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Update it + await factory.updateDoc(core.class.Space, core.space.Model, space, { + description: 'updated' + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Find should have updated version + const result = await liveQuery.findOne(core.class.Space, { _id: space }) + + expect(result).toBeDefined() + expect(result?.description).toBe('updated') + + await close() + }) + + it('should clean refs cache when query is removed', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'clean-refs', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Cache it + const callback = jest.fn() + const unsubscribe = liveQuery.query(core.class.Space, { _id: space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Unsubscribe to clean refs + unsubscribe() + + await new Promise((resolve) => setTimeout(resolve, 100)) + + await close() + }) + + it('should handle multiple queries referencing same document', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'multi-ref', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Multiple queries on same document + const callback1 = jest.fn() + const callback2 = jest.fn() + const callback3 = jest.fn() + + liveQuery.query(core.class.Space, { _id: space }, callback1) + liveQuery.query(core.class.Space, { _id: space }, callback2) + liveQuery.query(core.class.Space, { _id: space }, callback3) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback1).toHaveBeenCalled() + expect(callback2).toHaveBeenCalled() + expect(callback3).toHaveBeenCalled() + + await close() + }) + + it('should use refs cache for descendants check', async () => { + const { liveQuery, factory, close } = await getClient() + + // Create documents + const space1 = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'desc-test-1', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Cache with callback + const callback = jest.fn() + liveQuery.query(core.class.Space, {}, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // FindOne by _id should check descendants + const result = await liveQuery.findOne(core.class.Space, { _id: space1 }) + + expect(result).toBeDefined() + + await close() + }) +}) diff --git a/foundations/core/packages/query/src/__tests__/remaining-edge-cases.test.ts b/foundations/core/packages/query/src/__tests__/remaining-edge-cases.test.ts new file mode 100644 index 0000000000..d140cc3039 --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/remaining-edge-cases.test.ts @@ -0,0 +1,579 @@ +// Additional edge case tests for remaining uncovered code +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import core, { createClient, TxOperations } from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' +import { test } from './minmodel' + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx) => { + void liveQuery.tx(...tx) + } + return { + liveQuery, + factory: new TxOperations(storage, core.account.System), + close: async () => { + await liveQuery.close() + } + } +} + +describe('Edge Cases for Uncovered Code', () => { + describe('Reverse lookup update scenarios', () => { + it('should handle updates to documents in reverse lookup arrays', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'reverse-update-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const childComment = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'child original' + } + ) + + const callback = jest.fn() + + // Query with reverse lookup + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Update the child comment + await factory.updateCollection( + test.class.TestComment, + space, + childComment, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'child updated' + } + ) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + // Verify lookup data was updated + const lastResult = callback.mock.calls[callback.mock.calls.length - 1][0][0] + expect(lastResult.$lookup).toBeDefined() + + await close() + }) + + it('should handle mixin updates to documents in reverse lookup', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'mixin-reverse-lookup', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const childComment = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'child' + } + ) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Create mixin on child + await factory.createMixin(childComment, test.class.TestComment, space, test.mixin.TestMixin, {}) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback.mock.calls.length).toBeGreaterThanOrEqual(initialCalls) + + await close() + }) + }) + + describe('Regular lookup update scenarios', () => { + it('should update $lookup when linked document is updated via TxUpdateDoc', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'lookup-update-test', + description: 'original', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'test' + }) + + const callback = jest.fn() + + // Query with lookup to space + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { + space: core.class.Space + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Update the space (lookup target) + await factory.updateDoc(core.class.Space, core.space.Model, space, { + description: 'updated' + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + // Verify lookup was updated + const lastResult = callback.mock.calls[callback.mock.calls.length - 1][0][0] + expect(lastResult.$lookup?.space).toBeDefined() + + await close() + }) + + it('should update $lookup when linked document receives mixin via TxMixin', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'mixin-lookup-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'test' + }) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { + space: core.class.Space + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Add mixin to space + await factory.createMixin(space, core.class.Space, core.space.Model, test.mixin.TestMixin, {}) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback.mock.calls.length).toBeGreaterThanOrEqual(initialCalls) + + await close() + }) + }) + + describe('Array-based reverse lookup edge cases', () => { + it('should handle reverse lookup with array value containing undefined document', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'undefined-in-array', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + // Create child, get its ID, then we'll query before it's properly looked up + const childComment = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'child' + } + ) + + const callback = jest.fn() + + // Query with reverse lookup + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Try to update a non-existent child (index will be -1) + await factory.updateCollection( + test.class.TestComment, + space, + childComment, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'updated' + } + ) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + + it('should handle updates when reverse lookup value is not yet in array', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'not-in-array', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { comments: test.class.TestComment } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Create a child which should trigger adding to lookup array + const childComment = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'new child' + } + ) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Update it - should find it in array now + await factory.updateCollection( + test.class.TestComment, + space, + childComment, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'child updated' + } + ) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Nested lookup scenarios', () => { + it('should handle nested lookup where parent document exists', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'nested-exists', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const childComment = await factory.addCollection( + test.class.TestComment, + space, + parentComment, + test.class.TestComment, + 'comments', + { + message: 'child' + } + ) + + const callback = jest.fn() + + // Nested lookup: child -> attachedTo (parent) -> space + liveQuery.query(test.class.TestComment, { _id: childComment }, callback, { + lookup: { + attachedTo: [test.class.TestComment, { space: core.class.Space }] + } + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] + expect(result.$lookup?.attachedTo).toBeDefined() + + await close() + }) + + it('should handle nested lookup where parent document is undefined', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'nested-undefined', + description: 'test', + private: false, + members: [], + archived: false + }) + + // Create orphan comment with no valid parent + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'orphan' + }) + + const callback = jest.fn() + + // Try nested lookup where parent might not exist + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { + attachedTo: [core.class.Space, {}] // Will find space, nested is empty + } + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('Reverse lookup with custom attribute', () => { + it('should handle reverse lookup with custom attribute name (array format)', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'custom-attr', + description: 'test', + private: false, + members: [], + archived: false + }) + + const parentComment = await factory.addCollection( + test.class.TestComment, + space, + space, + core.class.Space, + 'comments', + { + message: 'parent' + } + ) + + const callback = jest.fn() + + // Reverse lookup with explicit attribute + liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { + lookup: { + _id: { + children: [test.class.TestComment, 'attachedTo'] + } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Add child + await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { + message: 'child with custom attr' + }) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + expect(callback).toHaveBeenCalled() + + const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] + expect(result.$lookup).toBeDefined() + + await close() + }) + }) + + describe('Lookup with mixin key checks', () => { + it('should handle lookup keys that might be mixin properties', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'mixin-key', + description: 'test', + private: false, + members: [], + archived: false + }) + + const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'test' + }) + + const callback = jest.fn() + + // Lookup with keys that will be checked via checkMixinKey + liveQuery.query(test.class.TestComment, { _id: comment }, callback, { + lookup: { + space: core.class.Space, + attachedTo: core.class.Space + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] + expect(result.$lookup?.space).toBeDefined() + + await close() + }) + }) + + describe('Reverse lookup with 0 or undefined values', () => { + it('should skip reverse lookup when field value is 0', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'zero-value', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Query with reverse lookup on a field that might be 0 or undefined + liveQuery.query(core.class.Space, { _id: space }, callback, { + lookup: { + _id: { + nonExistentField: test.class.TestComment + } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(callback).toHaveBeenCalled() + + // Should not crash and result should be valid + const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] + expect(result).toBeDefined() + + await close() + }) + }) +}) diff --git a/foundations/core/packages/query/src/__tests__/workspace-events.test.ts b/foundations/core/packages/query/src/__tests__/workspace-events.test.ts new file mode 100644 index 0000000000..6a92c88844 --- /dev/null +++ b/foundations/core/packages/query/src/__tests__/workspace-events.test.ts @@ -0,0 +1,371 @@ +// Workspace event handling tests +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import core, { + BulkUpdateEvent, + createClient, + IndexingUpdateEvent, + Ref, + TxOperations, + TxWorkspaceEvent, + WorkspaceEvent +} from '@hcengineering/core' +import { LiveQuery } from '..' +import { connect } from './connection' +import { test } from './minmodel' + +async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { + const storage = await createClient(connect) + const liveQuery = new LiveQuery(storage) + storage.notify = (...tx) => { + void liveQuery.tx(...tx) + } + return { + liveQuery, + factory: new TxOperations(storage, core.account.System), + close: async () => { + await liveQuery.close() + } + } +} + +describe('Workspace Events', () => { + describe('IndexingUpdate event with $search queries', () => { + it('should refresh queries with $search when IndexingUpdate event occurs', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'indexing-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'searchable message' + }) + + const callback = jest.fn() + + // Query with $search + liveQuery.query(test.class.TestComment, { $search: 'searchable' }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Send IndexingUpdate event + const params: IndexingUpdateEvent = { + _class: [test.class.TestComment] + } + const indexingEvent: TxWorkspaceEvent = { + _id: 'indexing-event' as Ref, + _class: core.class.TxWorkspaceEvent, + space: core.space.DerivedTx, + modifiedOn: Date.now(), + modifiedBy: core.account.System, + objectSpace: space, + event: WorkspaceEvent.IndexingUpdate, + params + } + + await liveQuery.tx(indexingEvent) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Should have triggered a refresh + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + + it('should handle IndexingUpdate for queued queries with $search', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'queue-indexing', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Create a query with $search + const unsubscribe = liveQuery.query(test.class.TestComment, { $search: 'test' }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Unsubscribe to move to queue + unsubscribe() + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Send IndexingUpdate event + const params: IndexingUpdateEvent = { + _class: [test.class.TestComment] + } + const indexingEvent: TxWorkspaceEvent = { + _id: 'indexing-queue-event' as Ref, + _class: core.class.TxWorkspaceEvent, + space: core.space.DerivedTx, + modifiedOn: Date.now(), + modifiedBy: core.account.System, + objectSpace: space, + event: WorkspaceEvent.IndexingUpdate, + params + } + + await liveQuery.tx(indexingEvent) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Query should have been handled (either removed or refreshed) + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('BulkUpdate event', () => { + it('should refresh queries when BulkUpdate event occurs for matching class', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'bulk-update', + description: 'test', + private: false, + members: [], + archived: false + }) + + await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'bulk test' + }) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, {}, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Send BulkUpdate event + const params: BulkUpdateEvent = { + _class: [test.class.TestComment] + } + const bulkEvent: TxWorkspaceEvent = { + _id: 'bulk-event' as Ref, + _class: core.class.TxWorkspaceEvent, + space: core.space.DerivedTx, + modifiedOn: Date.now(), + modifiedBy: core.account.System, + objectSpace: space, + event: WorkspaceEvent.BulkUpdate, + params + } + + await liveQuery.tx(bulkEvent) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Should have triggered a refresh + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + + it('should handle BulkUpdate for queued queries', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'queue-bulk', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Create and then unsubscribe to move to queue + const unsubscribe = liveQuery.query(test.class.TestComment, {}, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + unsubscribe() + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Send BulkUpdate event + const params: BulkUpdateEvent = { + _class: [test.class.TestComment] + } + const bulkEvent: TxWorkspaceEvent = { + _id: 'bulk-queue-event' as Ref, + _class: core.class.TxWorkspaceEvent, + space: core.space.DerivedTx, + modifiedOn: Date.now(), + modifiedBy: core.account.System, + objectSpace: space, + event: WorkspaceEvent.BulkUpdate, + params + } + + await liveQuery.tx(bulkEvent) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Query should have been handled + expect(callback).toHaveBeenCalled() + + await close() + }) + }) + + describe('SecurityChange event', () => { + it('should refresh queries when SecurityChange event occurs for matching space', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'security-test', + description: 'test', + private: false, + members: [], + archived: false + }) + + await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { + message: 'security test' + }) + + const callback = jest.fn() + + liveQuery.query(test.class.TestComment, { space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Send SecurityChange event + const securityEvent: TxWorkspaceEvent = { + _id: 'security-event' as Ref, + _class: core.class.TxWorkspaceEvent, + space: core.space.DerivedTx, + modifiedOn: Date.now(), + modifiedBy: core.account.System, + objectSpace: space, + event: WorkspaceEvent.SecurityChange, + params: null + } + + await liveQuery.tx(securityEvent) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Should have triggered a refresh + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + + it('should handle SecurityChange for queries with non-string space (e.g., $in)', async () => { + const { liveQuery, factory, close } = await getClient() + + const space1 = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'security-1', + description: 'test', + private: false, + members: [], + archived: false + }) + + const space2 = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'security-2', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Query with $in for space (non-string) + liveQuery.query(test.class.TestComment, { space: { $in: [space1, space2] } }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + const initialCalls = callback.mock.calls.length + + // Send SecurityChange event + const securityEvent: TxWorkspaceEvent = { + _id: 'security-nonstring-event' as Ref, + _class: core.class.TxWorkspaceEvent, + space: core.space.DerivedTx, + modifiedOn: Date.now(), + modifiedBy: core.account.System, + objectSpace: space1, + event: WorkspaceEvent.SecurityChange, + params: null + } + + await liveQuery.tx(securityEvent) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Should have triggered a refresh since space query is non-string + expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) + + await close() + }) + + it('should handle SecurityChange for queued queries', async () => { + const { liveQuery, factory, close } = await getClient() + + const space = await factory.createDoc(core.class.Space, core.space.Model, { + name: 'security-queue', + description: 'test', + private: false, + members: [], + archived: false + }) + + const callback = jest.fn() + + // Create and unsubscribe to move to queue + const unsubscribe = liveQuery.query(test.class.TestComment, { space }, callback) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + unsubscribe() + + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Send SecurityChange event + const securityEvent: TxWorkspaceEvent = { + _id: 'security-queue-event' as Ref, + _class: core.class.TxWorkspaceEvent, + space: core.space.DerivedTx, + modifiedOn: Date.now(), + modifiedBy: core.account.System, + objectSpace: space, + event: WorkspaceEvent.SecurityChange, + params: null + } + + await liveQuery.tx(securityEvent) + + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Query should have been handled + expect(callback).toHaveBeenCalled() + + await close() + }) + }) +}) diff --git a/foundations/core/packages/query/src/index.ts b/foundations/core/packages/query/src/index.ts new file mode 100644 index 0000000000..1ebe6bb662 --- /dev/null +++ b/foundations/core/packages/query/src/index.ts @@ -0,0 +1,1610 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Analytics } from '@hcengineering/analytics' +import core, { + Association, + BulkUpdateEvent, + Class, + Client, + DOMAIN_MODEL, + Doc, + DocumentQuery, + FindOptions, + FindResult, + Hierarchy, + IndexingUpdateEvent, + Lookup, + LookupData, + Mixin, + ModelDb, + Ref, + Relation, + ReverseLookups, + SearchOptions, + SearchQuery, + SearchResult, + SortingQuery, + Space, + Tx, + TxCreateDoc, + TxMixin, + TxProcessor, + TxRemoveDoc, + TxResult, + TxUpdateDoc, + TxWorkspaceEvent, + WithLookup, + WithTx, + WorkspaceEvent, + checkMixinKey, + clone, + findProperty, + generateId, + getObjectValue, + matchQuery, + platformNow, + reduceCalls, + shouldShowArchived, + toFindResult, + type DomainParams, + type DomainRequestOptions, + type DomainResult, + type OperationDomain +} from '@hcengineering/core' +import { PlatformError } from '@hcengineering/platform' +import { deepEqual } from 'fast-equals' +import { Refs } from './refs' +import { ResultArray } from './results' +import { Callback, Query, type QueryId } from './types' + +const CACHE_SIZE = 125 + +/** + * @public + */ +export class LiveQuery implements WithTx, Client { + private readonly client: Client + private readonly queries = new Map>, Map>() + private readonly queue = new Map() + private queryCounter: number = 0 + private closed: boolean = false + + private readonly queriesToUpdate = new Map() + + private readonly refs = new Refs(() => this.getHierarchy()) + + constructor (client: Client) { + this.client = client + } + + public isClosed (): boolean { + return this.closed + } + + async close (): Promise { + this.closed = true + await this.client.close() + } + + getHierarchy (): Hierarchy { + return this.client.getHierarchy() + } + + getModel (): ModelDb { + return this.client.getModel() + } + + // Perform refresh of content since connection established. + async refreshConnect (clean: boolean): Promise { + for (const q of [...this.queue.values()]) { + if (!this.removeFromQueue(q)) { + try { + if (clean) { + this.cleanQuery(q) + } + // No need to refresh, since it will be on next for + } catch (err: any) { + if (err instanceof PlatformError) { + if (err.message === 'connection closed') { + continue + } + } + Analytics.handleError(err) + console.error(err) + } + } else { + // No callbacks, let's remove it on conenct + this.removeQueue(q) + } + } + for (const v of this.queries.values()) { + for (const q of v.values()) { + try { + if (clean) { + this.cleanQuery(q) + } + void this.refresh(q) + } catch (err: any) { + if (err instanceof PlatformError) { + if (err.message === 'connection closed') { + continue + } + } + Analytics.handleError(err) + console.error(err) + } + } + } + } + + private cleanQuery (q: Query): void { + q.callbacks.forEach((callback) => { + callback(toFindResult([], 0)) + }) + q.result = new ResultArray([], this.getHierarchy()) + q.total = -1 + } + + private match (q: Query, doc: Doc, skipLookup = false): boolean { + if (this.getHierarchy().isMixin(q._class)) { + if (this.getHierarchy().hasMixin(doc, q._class)) { + doc = this.getHierarchy().as(doc, q._class) + } else { + return false + } + } + if (!this.getHierarchy().isDerived(doc._class, q._class)) { + // Check if it is not a mixin and not match class + const mixinClass = Hierarchy.mixinClass(doc) + if (mixinClass === undefined || !this.getHierarchy().isDerived(mixinClass, q._class)) { + return false + } + } + const query = q.query + for (const key in query) { + if (key === '$search') continue + if (skipLookup && key.startsWith('$lookup')) continue + const value = (query as any)[key] + const result = findProperty([doc], key, value) + if (result.length === 0) { + return false + } + } + return true + } + + private createDumpQuery( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Query { + const q = this.createQuery(_class, query, undefined, options) + this.queue.set(q.id, { ...q, lastUsed: platformNow() }) + if (!(q.result instanceof Promise)) { + q.result.clean() + } + return q + } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + if (this.client.getHierarchy().getDomain(_class) === DOMAIN_MODEL) { + return await this.client.findAll(_class, query, options) + } + const opt = { ...(options ?? {}) } + if (opt.projection !== undefined) { + opt.projection = { + ...opt.projection, + _class: 1, + space: 1, + modifiedOn: 1 + } + } + + // Perform one document queries if applicable. + const d = this.refs.findFromDocs(_class, query, opt) + if (d !== null) { + return d + } + + const q = this.findQuery(_class, query, opt) ?? this.createDumpQuery(_class, query, opt) + if (q.result instanceof Promise) { + q.result = await q.result + } + if (this.removeFromQueue(q, false)) { + this.queue.set(q.id, { ...q, lastUsed: platformNow() }) + q.result.clean() + } + return toFindResult(q.result.getClone(), q.total) + } + + async domainRequest( + domain: OperationDomain, + params: DomainParams, + options?: DomainRequestOptions + ): Promise> { + return await this.client.domainRequest(domain, params, options) + } + + searchFulltext (query: SearchQuery, options: SearchOptions): Promise { + return this.client.searchFulltext(query, options) + } + + async findOne( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise | undefined> { + if (this.client.getHierarchy().getDomain(_class) === DOMAIN_MODEL) { + return await this.client.findOne(_class, query, options) + } + if (options?.projection !== undefined) { + options.projection = { + ...options.projection, + _class: 1, + space: 1, + modifiedOn: 1 + } + } + + if (options === undefined) { + options = {} + } + options.limit = 1 + + const d = this.refs.findFromDocs(_class, query, options) + if (d !== null) { + return d[0] + } + + const q = this.findQuery(_class, query, options) ?? this.createDumpQuery(_class, query, options) + if (q.result instanceof Promise) { + q.result = await q.result + } + if (this.removeFromQueue(q, false)) { + this.queue.set(q.id, { ...q, lastUsed: platformNow() }) + q.result.clean() + } + return q.result.getClone>().shift() + } + + private optionsCompare (opt1?: FindOptions, opt2?: FindOptions): boolean { + const { ctx: _1, ..._opt1 } = (opt1 ?? {}) as any + const { ctx: _2, ..._opt2 } = (opt2 ?? {}) as any + return deepEqual(_opt1, _opt2) + } + + private queryCompare (q1: DocumentQuery, q2: DocumentQuery): boolean { + if (Object.keys(q1).length !== Object.keys(q2).length) { + return false + } + return deepEqual(q1, q2) + } + + private findQuery( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Query | undefined { + const queries = this.getQueueMap(_class) + if (queries === undefined) return + + for (const q of queries.values()) { + if (!this.queryCompare(query, q.query) || !this.optionsCompare(options, q.options)) continue + return q + } + } + + private removeFromQueue (q: Query, update = true): boolean { + if (q.callbacks.size === 0) { + const removed = this.queue.delete(q.id) + if (removed) { + if (update) { + if (!(q.result instanceof Promise)) { + this.refs.updateDocuments(q, q.result.getDocs(), true) + } + } + return true + } + } + return false + } + + private pushCallback ( + q: Query, + callback: { + callback: (result: Doc[]) => void + callbackId: string + } + ): void { + q.callbacks.set(callback.callbackId, callback.callback) + setTimeout(async () => { + if (q !== undefined) { + if (q.result instanceof Promise) { + q.result = await q.result + } + callback.callback(toFindResult(q.result.getResult(callback.callbackId), q.total)) + } + }, 0) + } + + private getQuery( + _class: Ref>, + query: DocumentQuery, + callback: { + callback: (result: Doc[]) => void + callbackId: string + }, + options?: FindOptions + ): Query | undefined { + const current = this.findQuery(_class, query, options) + if (current !== undefined) { + this.removeFromQueue(current, false) + this.pushCallback(current, callback) + + return current + } + } + + private getQueueMap (_class: Ref>): Map { + let cq = this.queries.get(_class) + if (cq === undefined) { + cq = new Map() + this.queries.set(_class, cq) + } + return cq + } + + private createQuery( + _class: Ref>, + query: DocumentQuery, + callback: { callback: (result: FindResult) => void, callbackId: string } | undefined, + options?: FindOptions + ): Query { + const _query: DocumentQuery = clone(query) + const localResult = this.refs.findFromDocs(_class, query, options) + const result = localResult != null ? Promise.resolve(localResult) : this.client.findAll(_class, query, options) + const q: Query = { + id: ++this.queryCounter, + _class, + query: _query, + result: result.then((docs) => new ResultArray(docs, this.getHierarchy())), + total: 0, + options: options as FindOptions, + callbacks: new Map(), + refresh: reduceCalls(() => this.doRefresh(q)), + refreshId: 0 + } + if (callback !== undefined) { + q.callbacks.set(callback.callbackId, callback.callback as unknown as Callback) + } + this.getQueueMap(_class).set(q.id, q) + result + .then(async (result) => { + q.total = result.total + await this.callback(q) + }) + .catch((err: any) => { + Analytics.handleError(err) + console.log('failed to update Live Query: ', err) + }) + + if (this.queue.size > CACHE_SIZE) { + this.remove() + } + return q + } + + private remove (): void { + const used = Array.from(this.queue.values()).sort((a, b) => a.lastUsed - b.lastUsed) + // Remove enough queries to bring the queue back down to 80% of CACHE_SIZE + // This prevents constant cleanup cycles + const targetSize = Math.floor(CACHE_SIZE * 0.8) + const toRemove = Math.max(0, this.queue.size - targetSize) + for (let i = 0; i < toRemove; i++) { + const q = used.shift() + if (q === undefined) return + this.removeQueue(q) + } + } + + removeQueue (q: Query): void { + const queries = this.getQueueMap(q._class) + const removed = queries.delete(q.id) + this.queue.delete(q.id) + if (removed) { + if (!(q.result instanceof Promise)) { + this.refs.updateDocuments(q, q.result.getDocs(), true) + } + } + } + + query( + _class: Ref>, + query: DocumentQuery, + callback: (result: FindResult) => void, + options?: FindOptions + ): () => void { + if (options?.projection !== undefined) { + options.projection = { + ...options.projection, + _class: 1, + space: 1, + modifiedOn: 1 + } + } + const callbackId = generateId() + const q = + this.getQuery(_class, query, { callback: callback as (result: Doc[]) => void, callbackId }, options) ?? + this.createQuery(_class, query, { callback, callbackId }, options) + + return () => { + q.callbacks.delete(callbackId) + if (q.callbacks.size === 0) { + if (!(q.result instanceof Promise)) { + q.result.clean() + } + this.queue.set(q.id, { ...q, lastUsed: platformNow() }) + // Check if we need to clean up the queue after adding this query + if (this.queue.size > CACHE_SIZE) { + this.remove() + } + } + } + } + + async queryFind( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + if (options?.projection !== undefined) { + options.projection = { + ...options.projection, + _class: 1, + space: 1, + modifiedOn: 1 + } + } + const current = this.findQuery(_class, query, options) + if (current === undefined) { + const q = this.createQuery( + _class, + query, + undefined, // No need of callback + options + ) + if (q.result instanceof Promise) { + q.result = await q.result + } + return toFindResult(q.result.getClone(), q.total) + } + if (current.result instanceof Promise) { + current.result = await current.result + } + return toFindResult(current.result.getClone(), current.total) + } + + private async checkSearch (q: Query, _id: Ref): Promise { + const match = await this.client.findOne(q._class, { $search: q.query.$search, _id }, q.options) + if (q.result instanceof Promise) { + q.result = await q.result + } + if (match === undefined) { + if (q.options?.limit === q.result.length) { + await this.refresh(q) + return true + } else { + const doc = q.result.delete(_id) + if (doc !== undefined) { + this.refs.updateDocuments(q, [doc], true) + if (q.options?.total === true) { + q.total-- + } + } + } + } else { + const doc = q.result.findDoc(_id) + if (doc !== undefined) { + q.result.updateDoc(match, false) + this.refs.updateDocuments(q, [match]) + } + } + return false + } + + private async getDocFromCache ( + docCache: Map, + _id: Ref, + _class: Ref>, + space: Ref, + q: Query + ): Promise { + const options: any = {} + if (q.options?.associations !== undefined) { + options.associations = q.options?.associations + } + if (q.options?.lookup !== undefined) { + options.lookup = q.options?.lookup + } + + const showArchived = shouldShowArchived(q.query, q.options) + + options.showArchived = showArchived + const docIdKey = _id + JSON.stringify(options ?? {}) + q._class + + const current = docCache.get(docIdKey) ?? (await this.client.findOne(q._class, { _id, space }, options)) + if (current !== undefined) { + docCache.set(docIdKey, current) + } else { + docCache.delete(docIdKey) + } + return current + } + + private asMixin (doc: Doc, mixin: Ref>): Doc { + if (this.getHierarchy().isMixin(mixin)) { + return this.getHierarchy().as(doc, mixin) + } + return doc + } + + private async getCurrentDoc ( + q: Query, + _id: Ref, + space: Ref, + docCache: Map + ): Promise { + let current = await this.getDocFromCache(docCache, _id, q._class, space, q) + if (q.result instanceof Promise) { + q.result = await q.result + } + + const pos = q.result.findDoc(_id) + if (current !== undefined) { + current = this.asMixin(current, q._class) + } + if (current !== undefined && this.match(q, current)) { + q.result.updateDoc(current, false) + this.refs.updateDocuments(q, [current]) + } else { + if (q.options?.limit === q.result.length) { + await this.refresh(q) + return true + } else if (pos !== undefined) { + q.result.delete(_id) + this.refs.updateDocuments(q, [pos], true) + if (q.options?.total === true) { + q.total-- + } + } + } + return false + } + + private async __updateMixinDoc (q: Query, updatedDoc: WithLookup, tx: TxMixin): Promise { + updatedDoc = TxProcessor.updateMixin4Doc(updatedDoc, tx) + + const ops = { + ...tx.attributes, + modifiedBy: tx.modifiedBy, + modifiedOn: tx.modifiedOn + } + await this.__updateLookup(q, updatedDoc, ops) + } + + private checkUpdatedDocMatch (q: Query, result: ResultArray, updatedDoc: WithLookup): boolean { + if (!this.match(q, updatedDoc)) { + if (q.options?.limit === result.length) { + void this.refresh(q) + return true + } else { + result.delete(updatedDoc._id) + this.refs.updateDocuments(q, [updatedDoc], true) + if (q.options?.total === true) { + q.total-- + } + } + } else { + result.updateDoc(updatedDoc, false) + this.refs.updateDocuments(q, [updatedDoc]) + } + return false + } + + protected async txMixin (tx: TxMixin, docCache: Map): Promise { + const hierarchy = this.client.getHierarchy() + + for (const queries of this.queries.entries()) { + const isTx = hierarchy.isDerived(queries[0], core.class.Tx) + + for (const q of queries[1].values()) { + if (isTx) { + // handle add since Txes are immutable + if (this.match(q, tx, q.options?.lookup !== undefined)) { + await this.handleDocAdd(q, tx, true, docCache) + } + await this.handleDocAddLookup(q, tx) + continue + } + if (q.result instanceof Promise) { + q.result = await q.result + } + let updatedDoc = q.result.findDoc(tx.objectId) + if (updatedDoc !== undefined) { + // If query contains search we must check use fulltext + if (q.query.$search != null && q.query.$search.length > 0) { + const searchRefresh = await this.checkSearch(q, tx.objectId) + if (searchRefresh) { + continue + } + } else { + if (updatedDoc.modifiedOn < tx.modifiedOn) { + await this.__updateMixinDoc(q, updatedDoc, tx) + updatedDoc = this.asMixin(updatedDoc, q._class) + const updateRefresh = this.checkUpdatedDocMatch(q, q.result, updatedDoc) + if (updateRefresh) { + continue + } + } else { + const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id, updatedDoc.space, docCache) + if (currentRefresh) { + continue + } + } + } + await this.sort(q, tx) + const udoc = q.result.findDoc(tx.objectId) + await this.updatedDocCallback(q, q.result, udoc) + } else if (queries[0] === tx.mixin) { + // Mixin potentially added to object we doesn't have in out results + const doc = await this.client.findOne(q._class, { ...q.query, _id: tx.objectId }, q.options) + if (doc !== undefined) { + if (this.match(q, doc, q.options?.lookup !== undefined)) { + await this.handleDocAdd(q, doc, false, docCache) + } + await this.handleDocAddLookup(q, doc) + } + } + await this.handleDocUpdateLookup(q, tx) + await this.handleDocUpdateRelation(q, tx) + } + } + return {} + } + + async txUpdateDoc (tx: TxUpdateDoc, docCache: Map): Promise { + for (const queries of this.queries.entries()) { + const isTx = this.client.getHierarchy().isDerived(queries[0], core.class.Tx) + for (const q of queries[1].values()) { + if (isTx) { + // handle add since Txes are immutable + // await this.handleDocAdd(q, tx, true, docCache) + if (this.match(q, tx, q.options?.lookup !== undefined)) { + await this.handleDocAdd(q, tx, true, docCache) + } + await this.handleDocAddLookup(q, tx) + continue + } + await this.handleDocUpdate(q, tx, docCache) + } + } + return {} + } + + private async handleDocUpdate (q: Query, tx: TxUpdateDoc, docCache: Map): Promise { + if (q.result instanceof Promise) { + q.result = await q.result + } + const updatedDoc = q.result.findDoc(tx.objectId) + if (updatedDoc !== undefined) { + // If query contains search we must check use fulltext + if (q.query.$search != null && q.query.$search.length > 0) { + const searchRefresh = await this.checkSearch(q, tx.objectId) + if (searchRefresh) return + } else { + if (updatedDoc.modifiedOn < tx.modifiedOn) { + await this.__updateDoc(q, updatedDoc, tx) + const updateRefresh = this.checkUpdatedDocMatch(q, q.result, updatedDoc) + if (updateRefresh) { + return + } + } else { + const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id, updatedDoc.space, docCache) + if (currentRefresh) { + return + } + } + } + await this.sort(q, tx) + const udoc = q.result.findDoc(tx.objectId) + await this.updatedDocCallback(q, q.result, udoc) + } else if (this.matchQuerySync(q, tx) && (await this.matchQuery(q, tx, docCache))) { + await this.sort(q, tx) + const udoc = q.result.findDoc(tx.objectId) + await this.updatedDocCallback(q, q.result, udoc) + } else if ( + this.client.getHierarchy().isDerived(tx.objectClass, q._class) && + q.options?.total === true && + q.options.limit === q.result.length + ) { + // we can make object is not matching criteria, but it can be in not limited results, total can be changed + await this.refresh(q) + return + } + await this.handleDocUpdateLookup(q, tx) + await this.handleDocUpdateRelation(q, tx) + } + + private isPossibleAssociationTx (tx: TxUpdateDoc | TxMixin, association: Association): boolean { + const h = this.getHierarchy() + const byClass = + h.isDerived(tx.objectClass, association.classA) || + h.isDerived(tx.objectClass, association.classB) || + h.isDerived(association.classA, tx.objectClass) || + h.isDerived(association.classB, tx.objectClass) + if (byClass) { + return true + } + if (tx._class === core.class.TxMixin) { + const mixinTx = tx as TxMixin + return h.isDerived(mixinTx.mixin, association.classA) || h.isDerived(mixinTx.mixin, association.classB) + } + return false + } + + private async handleDocUpdateRelation (q: Query, tx: TxUpdateDoc | TxMixin): Promise { + if (q.options?.associations === undefined) return + for (const assoc of q.options.associations) { + const association = this.getModel().findObject(assoc[0]) + if (association === undefined) continue + if (this.isPossibleAssociationTx(tx, association)) { + if (q.result instanceof Promise) { + q.result = await q.result + } + const docs = q.result.getDocs() + for (const doc of docs) { + const docToUpdate = doc.$associations?.[association._id]?.find((it) => it._id === tx.objectId) + if (docToUpdate !== undefined) { + if (tx._class === core.class.TxMixin) { + TxProcessor.updateMixin4Doc(docToUpdate, tx as TxMixin) + } else { + TxProcessor.updateDoc2Doc(docToUpdate, tx as TxUpdateDoc) + } + q.result.updateDoc(doc, false) + this.queriesToUpdate.set(q.id, q) + } + } + } + } + } + + private async handleDocUpdateLookup (q: Query, tx: TxUpdateDoc | TxMixin): Promise { + if (q.options?.lookup === undefined) return + const lookup = q.options.lookup + if (q.result instanceof Promise) { + q.result = await q.result + } + let needCallback = false + needCallback = await this.processLookupUpdateDoc(q.result, lookup, tx) + + if (needCallback) { + if (q.options?.sort !== undefined) { + q.result.sort(q._class, q.options.sort, this.getHierarchy(), this.client.getModel()) + } + await this.callback(q, true) + } + } + + private async processLookupUpdateDoc ( + docs: ResultArray, + lookup: Lookup, + tx: TxUpdateDoc | TxMixin + ): Promise { + let needCallback = false + const lookupWays = this.getLookupWays(lookup, tx.objectClass) + for (const lookupWay of lookupWays) { + const [objWay, key, reverseLookupKey] = lookupWay + for (const resDoc of docs.getDocs()) { + const obj = getObjectValue(objWay, resDoc) + if (obj === undefined) continue + const value = getObjectValue('$lookup.' + key, obj) + if (Array.isArray(value)) { + let index = value.findIndex((p) => p._id === tx.objectId) + if (this.client.getHierarchy().isDerived(tx.objectClass, core.class.AttachedDoc)) { + if (reverseLookupKey !== undefined) { + const reverseLookupValue = ( + tx._class === core.class.TxMixin + ? ((tx as TxMixin).attributes as any) + : ((tx as TxUpdateDoc).operations as any) + )[reverseLookupKey] + if (index !== -1 && reverseLookupValue !== undefined && reverseLookupValue !== obj._id) { + value.splice(index, 1) + index = -1 + needCallback = true + docs.updateDoc(resDoc, false) + } else if (index === -1 && reverseLookupValue === obj._id) { + const doc = await this.findOne(tx.objectClass, { _id: tx.objectId }) + if (doc !== undefined) { + value.push(doc) + index = value.length - 1 + } + needCallback = true + docs.updateDoc(resDoc, false) + } + } + } + if (index !== -1) { + if (tx._class === core.class.TxMixin) { + TxProcessor.updateMixin4Doc(value[index], tx as TxMixin) + } else { + TxProcessor.updateDoc2Doc(value[index], tx as TxUpdateDoc) + } + needCallback = true + docs.updateDoc(resDoc, false) + } + } else { + if (obj[key] === tx.objectId) { + if (obj.$lookup[key] !== undefined) { + if (tx._class === core.class.TxMixin) { + TxProcessor.updateMixin4Doc(obj.$lookup[key], tx as TxMixin) + } else { + TxProcessor.updateDoc2Doc(obj.$lookup[key], tx as TxUpdateDoc) + } + needCallback = true + docs.updateDoc(resDoc, false) + } + } + } + } + } + return needCallback + } + + private async refresh (q: Query): Promise { + this.queriesToUpdate.delete(q.id) + await q.refresh() + } + + private async doRefresh (q: Query): Promise { + const qid = ++q.refreshId + const res = await this.client.findAll(q._class, q.query, q.options) + if (q.refreshId === qid && (!deepEqual(res, q.result) || (res.total !== q.total && q.options?.total === true))) { + q.result = new ResultArray(res, this.getHierarchy()) + q.total = res.total + await this.callback(q) + } + } + + private matchQuerySync (q: Query, tx: TxUpdateDoc): boolean { + const clazz = this.getHierarchy().isMixin(q._class) ? this.getHierarchy().getBaseClass(q._class) : q._class + const target = (tx.operations as any)._class ?? tx.objectClass + if (!this.client.getHierarchy().isDerived(target, clazz)) { + return false + } + return true + } + + // Check if query is partially matched. + private async matchQuery (q: Query, tx: TxUpdateDoc, docCache: Map): Promise { + const doc: Doc = { + _id: tx.objectId, + _class: tx.objectClass, + modifiedBy: tx.modifiedBy, + modifiedOn: tx.modifiedOn, + space: tx.objectSpace + } + + // we cannot handle $inc correctly, let's skip it + const { $inc, ...ops } = tx.operations + + const emptyOps = Object.keys(ops).length === 0 + let matched = emptyOps || Object.keys(q.query).length === 0 + if (!emptyOps) { + const virtualTx = { + ...tx, + operations: ops + } + + TxProcessor.updateDoc2Doc(doc, virtualTx) + + for (const key in q.query) { + const value = (q.query as any)[key] + const tkey = checkMixinKey(key, q._class, this.client.getHierarchy()) + if ((doc as any)[tkey] === undefined) continue + const res = findProperty([doc], tkey, value) + if (res.length === 0) { + return false + } else { + matched = true + } + } + } + + if (matched) { + const realDoc = await this.getDocFromCache(docCache, doc._id, Hierarchy.mixinOrClass(doc), doc.space, q) + + if (realDoc == null) return false + + if (this.getHierarchy().isMixin(q._class)) { + if (!this.getHierarchy().hasMixin(realDoc, q._class)) { + return false + } + } + const res = matchQuery([realDoc], q.query, q._class, this.client.getHierarchy()) + if (res.length === 1) { + if (q.result instanceof Promise) { + q.result = await q.result + } + const doc = res[0] + const pos = q.result.findDoc(doc._id) + if (pos !== undefined) { + q.result.updateDoc(doc) + this.refs.updateDocuments(q, [doc]) + } else { + q.result.push(doc) + if (q.options?.total === true) { + q.total++ + } + } + return true + } + } + return false + } + + private async getLookupValue( + _class: Ref>, + doc: T, + lookup: Lookup, + result: LookupData + ): Promise { + for (const key in lookup) { + if (key === '_id') { + await this.getReverseLookupValue(doc, lookup, result) + continue + } + const value = (lookup as any)[key] + const tkey = checkMixinKey(key, _class, this.client.getHierarchy()) + if (Array.isArray(value)) { + const [_class, nested] = value + ;(result as any)[key] = await this.findOne(_class, { _id: getObjectValue(tkey, doc) }) + const nestedResult = {} + const parent = (result as any)[key] + if (parent !== undefined) { + await this.getLookupValue(_class, parent, nested, nestedResult) + Object.assign(parent, { + $lookup: nestedResult + }) + } + } else { + ;(result as any)[key] = await this.findOne(value, { _id: getObjectValue(tkey, doc) }) + } + } + } + + private async getReverseLookupValue( + doc: T, + lookup: ReverseLookups, + result: LookupData + ): Promise { + for (const key in lookup._id) { + if ((doc as any)[key] === undefined || (doc as any)[key] === 0) { + continue + } + + const value = lookup._id[key] + + let _class: Ref> + let attr = 'attachedTo' + + if (Array.isArray(value)) { + _class = value[0] + attr = value[1] + } else { + _class = value + } + ;(result as any)[key] = await this.findAll(_class, { [attr]: doc._id }) + } + } + + private async lookup(_class: Ref>, doc: T, lookup: Lookup): Promise { + const result: LookupData = {} + await this.getLookupValue(_class, doc, lookup, result) + ;(doc as WithLookup).$lookup = result + } + + protected async txCreateDoc (tx: TxCreateDoc, docCache: Map): Promise { + const docTx = TxProcessor.createDoc2Doc(tx) + for (const queries of this.queries.entries()) { + const doc = this.client.getHierarchy().isDerived(queries[0], core.class.Tx) ? tx : docTx + for (const q of queries[1].values()) { + // await this.handleDocAdd(q, doc, true, docCache) + if (this.match(q, doc, q.options?.lookup !== undefined)) { + await this.handleDocAdd(q, doc, true, docCache) + } + + await this.handleDocAddLookup(q, doc) + await this.handleDocAddRelation(q, doc) + } + } + return {} + } + + private async handleDocAdd (q: Query, doc: Doc, handleLookup = true, docCache: Map): Promise { + let needPush = true + if (q.result instanceof Promise) { + q.result = await q.result + } + if (q.options?.lookup !== undefined && handleLookup) { + await this.lookup(q._class, doc, q.options.lookup) + const matched = this.match(q, doc) + if (!matched) needPush = false + } + if (needPush) { + // We could already have document inside results, if query is created during processing of document create transaction and not yet handled on client. + const pos = q.result.findDoc(doc._id) + if (pos !== undefined) { + // No need to update, document already in results. + needPush = false + } + } + if (needPush) { + // If query contains search we must check use fulltext + if (q.query.$search != null && q.query.$search.length > 0) { + const match = await this.client.findOne(q._class, { $search: q.query.$search, _id: doc._id }, q.options) + if (match === undefined) return + } + + q.result.push(doc) + if (q.options?.total === true) { + q.total++ + } + + if (q.options?.sort !== undefined) { + q.result.sort(q._class, q.options.sort, this.getHierarchy(), this.client.getModel()) + } + + if (q.options?.limit !== undefined && q.result.length > q.options.limit) { + if (q.result.pop()?._id !== doc._id || q.options?.total === true) { + await this.callback(q, true) + } + } else { + await this.callback(q, true) + } + } + } + + private async callback (q: Query, bulkUpdate = false): Promise { + if (q.result instanceof Promise) { + q.result = await q.result + } + + const result = q.result + + this.refs.updateDocuments(q, result.getDocs()) + + if (bulkUpdate) { + this.queriesToUpdate.set(q.id, q) + } else { + this.queriesToUpdate.delete(q.id) + for (const [id, callback] of q.callbacks.entries()) { + callback(toFindResult(result.getResult(id), q.total)) + } + } + } + + private async handleDocAddRelation (q: Query, doc: Doc): Promise { + if (q.options?.associations === undefined) return + if (doc._class !== core.class.Relation) return + const relation = doc as Relation + const assoc = q.options.associations.find((p) => p[0] === relation.association) + if (assoc !== undefined) { + if (q.result instanceof Promise) { + q.result = await q.result + } + const direct = assoc[1] === 1 + const res = q.result.findDoc(direct ? relation.docA : relation.docB) + if (res === undefined) return + const association = this.getModel().findObject(assoc[0]) + if (association === undefined) return + const docToPush = await this.findOne(direct ? association.classB : association.classA, { + _id: direct ? relation.docB : relation.docA + }) + if (docToPush === undefined) return + const arr = res?.$associations?.[relation.association] ?? [] + arr.push(docToPush) + if (res?.$associations === undefined) { + res.$associations = {} + } + res.$associations[relation.association] = arr + q.result.updateDoc(res, false) + this.queriesToUpdate.set(q.id, q) + } + } + + private async handleDocAddLookup (q: Query, doc: Doc): Promise { + if (q.options?.lookup === undefined) return + const lookup = q.options.lookup + if (q.result instanceof Promise) { + q.result = await q.result + } + let needCallback = false + needCallback = this.proccesLookupAddDoc(q.result, lookup, doc) + + if (needCallback) { + if (q.options?.sort !== undefined) { + q.result.sort(q._class, q.options.sort, this.getHierarchy(), this.client.getModel()) + } + await this.callback(q, true) + } + } + + private proccesLookupAddDoc (docs: ResultArray, lookup: Lookup, doc: Doc): boolean { + let needCallback = false + const lookupWays = this.getLookupWays(lookup, doc._class) + for (const lookupWay of lookupWays) { + const [objWay, key, reverseLookupKey] = lookupWay + for (const resDoc of docs.getDocs()) { + const obj = getObjectValue(objWay, resDoc) + if (obj === undefined) continue + let value = getObjectValue('$lookup.' + key, obj) + const reverseCheck = reverseLookupKey !== undefined && (doc as any)[reverseLookupKey] === obj._id + if (value == null && reverseCheck) { + value = [] + obj.$lookup[key] = value + needCallback = true + docs.updateDoc(resDoc, false) + } + if (Array.isArray(value)) { + if (this.client.getHierarchy().isDerived(doc._class, core.class.AttachedDoc) && reverseCheck) { + const idx = (value as Doc[]).findIndex((p) => p._id === doc._id) + if (idx === -1) { + value.push(doc) + } else { + value[idx] = doc + } + needCallback = true + docs.updateDoc(resDoc, false) + } + } else { + if (obj[key] === doc._id) { + obj.$lookup[key] = doc + needCallback = true + docs.updateDoc(resDoc, false) + } + } + } + } + return needCallback + } + + protected async txRemoveDoc (tx: TxRemoveDoc, docCache: Map): Promise { + for (const queries of this.queries.entries()) { + const isTx = this.client.getHierarchy().isDerived(queries[0], core.class.Tx) + for (const q of queries[1].values()) { + if (isTx) { + // handle add since Txes are immutable + // await this.handleDocAdd(q, tx, true, docCache) + if (this.match(q, tx, q.options?.lookup !== undefined)) { + await this.handleDocAdd(q, tx, true, docCache) + } + + await this.handleDocAddLookup(q, tx) + continue + } + await this.handleDocRemove(q, tx) + } + } + return {} + } + + private async handleDocRemove (q: Query, tx: TxRemoveDoc): Promise { + const h = this.client.getHierarchy() + if (q._class === tx.objectClass || h.isDerived(q._class, tx.objectClass) || h.isDerived(tx.objectClass, q._class)) { + if (q.result instanceof Promise) { + q.result = await q.result + } + const index = q.result.getDocs().find((p) => p._id === tx.objectId) + if (index !== undefined) { + if (q.options?.limit !== undefined && q.options.limit === q.result.length && q.query._id !== tx.objectId) { + await this.refresh(q) + return + } + q.result.delete(index._id) + this.refs.updateDocuments(q, [index], true) + + if (q.options?.total === true) { + q.total-- + } + await this.callback(q, true) + } + } + await this.handleDocRemoveLookup(q, tx) + await this.handleDocRemoveRelation(q, tx) + } + + private async handleDocRemoveRelation (q: Query, tx: TxRemoveDoc): Promise { + if (q.options?.associations === undefined) return + if (tx.objectClass !== core.class.Relation) return + await this.refresh(q) + } + + private async handleDocRemoveLookup (q: Query, tx: TxRemoveDoc): Promise { + if (q.options?.lookup === undefined) return + let needCallback = false + const lookupWays = this.getLookupWays(q.options.lookup, tx.objectClass) + if (lookupWays.length === 0) return + if (q.result instanceof Promise) { + q.result = await q.result + } + for (const lookupWay of lookupWays) { + const [objWay, key] = lookupWay + const docs = q.result + for (const doc of docs.getDocs()) { + const obj = getObjectValue(objWay, doc) + if (obj === undefined) continue + const value = getObjectValue('$lookup.' + key, obj) + if (value === undefined) continue + if (Array.isArray(value)) { + const index = value.findIndex((p) => p._id === tx.objectId) + if (index !== -1) { + value.splice(index, 1) + needCallback = true + docs.updateDoc(doc, false) + } + } else { + if (value._id === tx.objectId) { + obj.$lookup[key] = undefined + needCallback = true + docs.updateDoc(doc, false) + } + } + } + } + if (needCallback) { + if (q.options?.sort !== undefined) { + q.result.sort(q._class, q.options.sort, this.getHierarchy(), this.client.getModel()) + } + await this.callback(q, true) + } + } + + private getLookupWays ( + lookup: Lookup, + _class: Ref>, + parent: string = '' + ): [string, string, string?][] { + const result: [string, string, string?][] = [] + const hierarchy = this.client.getHierarchy() + if (lookup._id !== undefined) { + for (const key in lookup._id) { + const value = (lookup._id as any)[key] + const [valueClass, reverseLookupKey] = Array.isArray(value) ? value : [value, 'attachedTo'] + const clazz = hierarchy.isMixin(valueClass) ? hierarchy.getBaseClass(valueClass) : valueClass + if (hierarchy.isDerived(_class, clazz)) { + result.push([parent, key, reverseLookupKey]) + } + } + } + for (const key in lookup) { + if (key === '_id') continue + const value = (lookup as any)[key] + if (Array.isArray(value)) { + const clazz = hierarchy.isMixin(value[0]) ? hierarchy.getBaseClass(value[0]) : value[0] + if (hierarchy.isDerived(_class, clazz)) { + result.push([parent, key]) + } + const lookupKey = '$lookup.' + key + const newParent = parent.length > 0 ? parent + '.' + lookupKey : lookupKey + const nested = this.getLookupWays(value[1], _class, newParent) + if (nested.length > 0) { + result.push(...nested) + } + } else { + const clazz = hierarchy.isMixin(value) ? hierarchy.getBaseClass(value) : value + if (hierarchy.isDerived(_class, clazz)) { + result.push([parent, key]) + } + } + } + return result + } + + async _tx (tx: Tx, docCache: Map): Promise { + switch (tx._class) { + case core.class.TxCreateDoc: + return await this.txCreateDoc(tx as TxCreateDoc, docCache) + case core.class.TxUpdateDoc: + return await this.txUpdateDoc(tx as TxUpdateDoc, docCache) + case core.class.TxRemoveDoc: + return await this.txRemoveDoc(tx as TxRemoveDoc, docCache) + case core.class.TxMixin: + return await this.txMixin(tx as TxMixin, docCache) + case core.class.TxApplyIf: + return await Promise.resolve([]) + } + return {} + } + + async tx (...txes: Tx[]): Promise { + const result: TxResult[] = [] + const docCache = new Map() + for (const tx of txes) { + if (tx._class === core.class.TxWorkspaceEvent) { + const evt = tx as TxWorkspaceEvent + await this.checkUpdateEvents(evt) + await this.changePrivateHandler(evt) + } + result.push(await this._tx(tx, docCache)) + } + + if (this.queriesToUpdate.size > 0) { + const copy = new Map(this.queriesToUpdate) + this.queriesToUpdate.clear() + + for (const q of copy.values()) { + if (q.result instanceof Promise) { + q.result = await q.result + } + const qr = q.result + for (const [id, callback] of q.callbacks.entries()) { + callback(toFindResult(qr.getResult(id), q.total)) + } + } + } + return result + } + + private async checkUpdateEvents (evt: TxWorkspaceEvent, trigger = true): Promise { + const h = this.client.getHierarchy() + function hasClass (q: Query, classes: Ref>[]): boolean { + return classes.includes(q._class) || classes.some((it) => h.isDerived(q._class, it) || h.isDerived(it, q._class)) + } + if (evt.event === WorkspaceEvent.IndexingUpdate) { + const indexingParam = evt.params as IndexingUpdateEvent + for (const q of [...this.queue.values()]) { + if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { + if (!this.removeFromQueue(q)) { + try { + await this.refresh(q) + } catch (err: any) { + Analytics.handleError(err) + console.error(err) + } + } else { + this.removeQueue(q) + } + } + } + for (const v of this.queries.values()) { + for (const q of v.values()) { + if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { + try { + await this.refresh(q) + } catch (err: any) { + Analytics.handleError(err) + console.error(err) + } + } + } + } + } + if (evt.event === WorkspaceEvent.BulkUpdate) { + const params = evt.params as BulkUpdateEvent + for (const q of [...this.queue.values()]) { + if (hasClass(q, params._class)) { + if (!this.removeFromQueue(q)) { + try { + await this.refresh(q) + } catch (err: any) { + Analytics.handleError(err) + console.error(err) + } + } + } + } + for (const v of this.queries.values()) { + for (const q of v.values()) { + if (hasClass(q, params._class)) { + try { + await this.refresh(q) + } catch (err: any) { + Analytics.handleError(err) + console.error(err) + } + } + } + } + } + } + + private async changePrivateHandler (evt: TxWorkspaceEvent): Promise { + if (evt.event === WorkspaceEvent.SecurityChange) { + for (const q of [...this.queue.values()]) { + if (typeof q.query.space !== 'string' || q.query.space === evt.objectSpace) { + if (!this.removeFromQueue(q)) { + try { + await this.refresh(q) + } catch (err: any) { + Analytics.handleError(err) + console.error(err) + } + } + } + } + for (const v of this.queries.values()) { + for (const q of v.values()) { + if (typeof q.query.space !== 'string' || q.query.space === evt.objectSpace) { + try { + await this.refresh(q) + } catch (err: any) { + Analytics.handleError(err) + console.error(err) + } + } + } + } + } + } + + private async __updateLookup (q: Query, updatedDoc: WithLookup, ops: any): Promise { + for (const key in ops) { + if (!key.startsWith('$')) { + if (q.options !== undefined) { + const lookup = (q.options.lookup as any)?.[key] + if (lookup !== undefined) { + const lookupClass = getLookupClass(lookup) + const nestedLookup = getNestedLookup(lookup) + if (Array.isArray(ops[key])) { + ;(updatedDoc.$lookup as any)[key] = await this.findAll( + lookupClass, + { _id: { $in: ops[key] } }, + { lookup: nestedLookup } + ) + } else { + ;(updatedDoc.$lookup as any)[key] = await this.findOne( + lookupClass, + { _id: ops[key] }, + { lookup: nestedLookup } + ) + } + } + } + } else { + if (key === '$push') { + const pops = ops[key] ?? {} + for (const pkey of Object.keys(pops)) { + if (q.options !== undefined) { + const lookup = (q.options.lookup as any)?.[pkey] + if (lookup !== undefined) { + const lookupClass = getLookupClass(lookup) + const nestedLookup = getNestedLookup(lookup) + const pp = updatedDoc.$lookup as any + if (pp[pkey] === undefined) { + pp[pkey] = [] + } + if (Array.isArray(pops[pkey])) { + const pushData = await this.findAll( + lookupClass, + { _id: { $in: pops[pkey] } }, + { lookup: nestedLookup } + ) + pp[pkey].push(...pushData) + } else { + const d = await this.findOne(lookupClass, { _id: pops[pkey] }, { lookup: nestedLookup }) + if (d !== undefined) { + pp[pkey].push(d) + } + } + } + } + } + } else if (key === '$pull') { + const pops = ops[key] ?? {} + for (const pkey of Object.keys(pops)) { + if (q.options !== undefined) { + const lookup = (q.options.lookup as any)?.[pkey] + if (lookup !== undefined) { + const pid = pops[pkey] + const pp = updatedDoc.$lookup as any + if (pp[pkey] === undefined) { + pp[pkey] = [] + } + if (Array.isArray(pid)) { + pp[pkey] = pp[pkey].filter((it: Doc) => !pid.includes(it._id)) + } else { + pp[pkey] = pp[pkey].filter((it: Doc) => it._id !== pid) + } + } + } + } + } + } + } + } + + private async __updateDoc (q: Query, updatedDoc: WithLookup, tx: TxUpdateDoc): Promise { + TxProcessor.updateDoc2Doc(updatedDoc, tx) + + const ops = { + ...tx.operations, + modifiedBy: tx.modifiedBy, + modifiedOn: tx.modifiedOn + } + await this.__updateLookup(q, updatedDoc, ops) + } + + private async sort (q: Query, tx: TxUpdateDoc | TxMixin): Promise { + const sort = q.options?.sort + if (sort === undefined) return + let needSort = sort.modifiedBy !== undefined || sort.modifiedOn !== undefined + if (!needSort) needSort = this.checkNeedSort(sort, tx) + + if (needSort) { + if (q.result instanceof Promise) { + q.result = await q.result + } + q.result.sort(q._class, sort, this.getHierarchy(), this.client.getModel()) + } + } + + private checkNeedSort (sort: SortingQuery, tx: TxUpdateDoc | TxMixin): boolean { + const ops = + tx._class === core.class.TxMixin + ? (tx as TxMixin).attributes + : ((tx as TxUpdateDoc).operations as any) + for (const key in ops) { + if (key.startsWith('$')) { + for (const opKey in ops[key]) { + if (opKey in sort) return true + } + } else { + if (key in sort) return true + } + } + return false + } + + private async updatedDocCallback (q: Query, res: ResultArray, updatedDoc: Doc | undefined): Promise { + if (q.options?.limit !== undefined && res.length > q.options.limit) { + if (updatedDoc === undefined) { + await this.refresh(q) + return + } + if (res.getDocs()[q.options?.limit]._id === updatedDoc._id) { + await this.refresh(q) + return + } + if (res.pop()?._id !== updatedDoc._id) { + await this.callback(q, true) + } + } else { + await this.callback(q, true) + } + } +} + +function getNestedLookup (lookup: Ref> | [Ref>, Lookup]): Lookup | undefined { + return Array.isArray(lookup) ? lookup[1] : undefined +} + +function getLookupClass (lookup: Ref> | [Ref>, Lookup]): Ref> { + return Array.isArray(lookup) ? lookup[0] : lookup +} diff --git a/foundations/core/packages/query/src/refs.ts b/foundations/core/packages/query/src/refs.ts new file mode 100644 index 0000000000..a044270328 --- /dev/null +++ b/foundations/core/packages/query/src/refs.ts @@ -0,0 +1,119 @@ +import { + Hierarchy, + matchQuery, + toFindResult, + type Class, + type Doc, + type DocumentQuery, + type FindOptions, + type FindResult, + type Ref, + type Timestamp +} from '@hcengineering/core' +import type { Query, QueryId } from './types' + +export interface DocumentRef { + doc: Doc + queries: QueryId[] + lastUsed: Timestamp +} + +export class Refs { + // A map of _class to documents. + private readonly documentRefs = new Map, DocumentRef>>() + + constructor (readonly getHierarchy: () => Hierarchy) {} + + public updateDocuments (q: Query, docs: Doc[], clean: boolean = false): void { + if (q.options?.projection !== undefined) { + return + } + const params = ':' + JSON.stringify(q.options?.lookup ?? {}) + ':' + JSON.stringify(q.options?.associations ?? {}) + for (const d of docs) { + const classKey = Hierarchy.mixinOrClass(d) + params + + let docMap = this.documentRefs.get(classKey) + if (docMap === undefined) { + if (clean) { + continue + } + docMap = new Map() + this.documentRefs.set(classKey, docMap) + } + const queries = (docMap.get(d._id)?.queries ?? []).filter((it) => it !== q.id) + if (!clean) { + queries.push(q.id) + } + if (queries.length === 0) { + docMap.delete(d._id) + } else { + const q = docMap.get(d._id) + if ((q?.lastUsed ?? 0) < d.modifiedOn) { + docMap.set(d._id, { ...(q ?? {}), doc: d, queries, lastUsed: d.modifiedOn }) + } + } + } + } + + public findFromDocs( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): FindResult | null { + if (typeof query._id === 'string') { + const desc = this.getHierarchy().getDescendants(_class) + for (const des of desc) { + const classKey = + des + ':' + JSON.stringify(options?.lookup ?? {}) + ':' + JSON.stringify(options?.associations ?? {}) + // One document query + const doc = this.documentRefs.get(classKey)?.get(query._id)?.doc + if (doc !== undefined) { + const q = matchQuery([doc], query, _class, this.getHierarchy()) + if (q.length > 0) { + return toFindResult(this.getHierarchy().clone([doc]), 1) + } + } + } + } + if ( + options?.limit === 1 && + options.total !== true && + options?.sort === undefined && + options?.projection === undefined + ) { + const classKey = + _class + ':' + JSON.stringify(options?.lookup ?? {}) + ':' + JSON.stringify(options?.associations ?? {}) + const docs = this.documentRefs.get(classKey) + if (docs !== undefined) { + const _docs = Array.from(docs.values()).map((it) => it.doc) + + const q = matchQuery(_docs, query, _class, this.getHierarchy()) + if (q.length > 0) { + return toFindResult(this.getHierarchy().clone([q[0]]), 1) + } + } + if (options.lookup === undefined && options.associations === undefined) { + const keys = Array.from(this.documentRefs.keys()) + for (const key of keys) { + if (key.startsWith(_class + ':')) { + const docs = this.documentRefs.get(key) + if (docs !== undefined) { + const _docs = Array.from(docs.values()).map((it) => it.doc) + + const q = matchQuery(_docs, query, _class, this.getHierarchy()) + if (q.length > 0) { + const clonedDoc = this.getHierarchy().clone(q[0]) + const { $lookup, $associations, ...clean } = clonedDoc + if (this.getHierarchy().isMixin(_class)) { + return toFindResult([this.getHierarchy().as(clean, _class)] as T[], 1) + } + return toFindResult([clean], 1) + } + } + } + } + } + } + return null + } +} diff --git a/foundations/core/packages/query/src/results.ts b/foundations/core/packages/query/src/results.ts new file mode 100644 index 0000000000..bc10576de8 --- /dev/null +++ b/foundations/core/packages/query/src/results.ts @@ -0,0 +1,100 @@ +import { + resultSort, + WithLookup, + type Class, + type Doc, + type Hierarchy, + type MemDb, + type Ref, + type SortingQuery +} from '@hcengineering/core' + +export class ResultArray { + private docs: Map, WithLookup> + + private readonly clones = new Map, WithLookup>>() + + get length (): number { + return this.docs.size + } + + constructor ( + docs: Doc[], + readonly hierarchy: Hierarchy + ) { + this.docs = new Map(docs.map((it) => [it._id, it])) + } + + clean (): void { + this.clones.clear() + } + + getDocs (): WithLookup[] { + return Array.from(this.docs.values()) + } + + findDoc (_id: Ref): WithLookup | undefined { + return this.docs.get(_id) + } + + getClone(): T[] { + return this.hierarchy.clone(this.getDocs()) + } + + getResult (id: string): Doc[] { + // Lets form a new list based on clones we have already. + const info = this.clones.get(id) + if (info === undefined) { + const docs = this.getClone() + this.clones.set(id, new Map(docs.map((it) => [it._id, it]))) + return docs + } else { + return Array.from(info.values()) + } + } + + delete (_id: Ref): Doc | undefined { + const doc = this.docs.get(_id) + this.docs.delete(_id) + for (const [, v] of this.clones.entries()) { + v.delete(_id) + } + return doc + } + + updateDoc (doc: WithLookup, mainClone = true): void { + this.docs.set(doc._id, mainClone ? this.hierarchy.clone(doc) : doc) + for (const [, v] of this.clones.entries()) { + v.set(doc._id, this.hierarchy.clone(doc)) + } + } + + push (doc: WithLookup): void { + this.docs.set(doc._id, this.hierarchy.clone(doc)) + for (const [, v] of this.clones.entries()) { + v.set(doc._id, this.hierarchy.clone(doc)) + } + // this.changes.add(doc._id) + } + + pop (): WithLookup | undefined { + const lastElement = Array.from(this.docs)[this.docs.size - 1] + if (lastElement !== undefined) { + this.docs.delete(lastElement[0]) + for (const [, v] of this.clones.entries()) { + v.delete(lastElement[0]) + } + return lastElement[1] + } + return undefined + } + + sort(_class: Ref>, sort: SortingQuery, hierarchy: Hierarchy, memdb: MemDb): void { + const docs = Array.from(this.docs.values()) + resultSort(docs, sort, _class, hierarchy, memdb) + this.docs = new Map(docs.map((it) => [it._id, it])) + for (const [k, v] of this.clones.entries()) { + this.clones.set(k, new Map(docs.map((it) => [it._id, v.get(it._id) ?? this.hierarchy.clone(it)]))) + } + } +} diff --git a/foundations/core/packages/query/src/types.ts b/foundations/core/packages/query/src/types.ts new file mode 100644 index 0000000000..5dec89ed02 --- /dev/null +++ b/foundations/core/packages/query/src/types.ts @@ -0,0 +1,17 @@ +import type { Class, Doc, DocumentQuery, FindOptions, FindResult, Ref } from '@hcengineering/core' +import type { ResultArray } from './results' + +export type Callback = (result: FindResult) => void + +export type QueryId = number +export interface Query { + id: QueryId // uniq query identifier. + _class: Ref> + query: DocumentQuery + result: ResultArray | Promise + options?: FindOptions + total: number + callbacks: Map + refresh: () => Promise + refreshId: number +} diff --git a/foundations/core/packages/query/tsconfig.json b/foundations/core/packages/query/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/query/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/rank/.eslintrc.js b/foundations/core/packages/rank/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/rank/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/rank/CHANGELOG.json b/foundations/core/packages/rank/CHANGELOG.json new file mode 100644 index 0000000000..5518e2c82b --- /dev/null +++ b/foundations/core/packages/rank/CHANGELOG.json @@ -0,0 +1,63 @@ +{ + "name": "@hcengineering/rank", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/rank_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/rank_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/rank_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/rank_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/rank/CHANGELOG.md b/foundations/core/packages/rank/CHANGELOG.md new file mode 100644 index 0000000000..4e97a1317e --- /dev/null +++ b/foundations/core/packages/rank/CHANGELOG.md @@ -0,0 +1,28 @@ +# Change Log - @hcengineering/rank + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/rank/config/rig.json b/foundations/core/packages/rank/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/rank/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/rank/jest.config.js b/foundations/core/packages/rank/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/rank/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/rank/package.json b/foundations/core/packages/rank/package.json new file mode 100644 index 0000000000..5103c831ed --- /dev/null +++ b/foundations/core/packages/rank/package.json @@ -0,0 +1,57 @@ +{ + "name": "@hcengineering/rank", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "_phase:build": "compile transpile src", + "_phase:validate": "compile validate", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "build": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "build:watch": "compile", + "format": "format src" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "eslint": "^8.54.0", + "prettier": "^3.6.2", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "typescript": "^5.9.3", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22", + "lexorank": "~1.0.4" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/rank/src/__tests__/utils.test.ts b/foundations/core/packages/rank/src/__tests__/utils.test.ts new file mode 100644 index 0000000000..d077ea93e7 --- /dev/null +++ b/foundations/core/packages/rank/src/__tests__/utils.test.ts @@ -0,0 +1,95 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { makeRank } from '..' + +describe('makeRank', () => { + it('calculates rank when no prev and next', () => { + expect(makeRank(undefined, undefined)).toBe('0|hzzzzz:') + }) + + it('check rank on empty string', () => { + expect(makeRank(undefined, '')).toBe('0|hzzzzz:') + }) + + it.each([ + ['0|hzzzzz:', '0|i00007:'], + ['0|i00007:', '0|i0000f:'], + ['0|i0000f:', '0|i0000n:'], + ['0|zzzzzz:', '0|zzzzzz:'] + ])('calculates rank value when prev is %p', (prev, expected) => { + expect(makeRank(prev, undefined)).toBe(expected) + }) + + it.each([ + ['0|hzzzzz:', '0|hzzzzr:'], + ['0|hzzzzr:', '0|hzzzzj:'], + ['0|hzzzzj:', '0|hzzzzb:'], + ['0|000000:', '0|000000:'] + ])('calculates rank value when next is %p', (next, expected) => { + expect(makeRank(undefined, next)).toBe(expected) + }) + + it.each([ + ['0|000000:', '0|000001:', '0|000000:i'], + ['0|hzzzzz:', '0|i0000f:', '0|i00007:'], + ['0|hzzzzz:', '0|hzzzzz:', '0|i00007:'], + ['0|i00007:', '0|i00007:', '0|i0000f:'], + ['0|i00007:', '0|i00008:', '0|i00007:i'] + ])('calculates rank value when prev is %p and next is %p', (prev, next, expected) => { + expect(makeRank(prev, next)).toBe(expected) + }) + + it.each([ + [10, '0|hzzzxr:'], + [100, '0|hzzzdr:'], + [1000, '0|hzzttr:'], + [10000, '0|hzya9r:'] + ])('produces prev rank of reasonable length for %p generations', (count, expected) => { + let rank = '0|hzzzzz:' + for (let i = 0; i < count; i++) { + rank = makeRank(undefined, rank) + } + expect(rank).toBe(expected) + }) + + it.each([ + [5, '0|zfqzzz:'], + [10, '0|zzd7vh:'], + [50, '0|zzzzzy:zzzi'], + [100, '0|zzzzzy:zzzzzzzzzzzv'] + ])('produces middle rank of reasonable length for %p generations', (count, expected) => { + let rank = '0|hzzzzz:' + for (let i = 0; i < count; i++) { + rank = makeRank(rank, '0|zzzzzz') + } + expect(rank).toBe(expected) + }) + + it.each([ + [10, '0|i00027:'], + [100, '0|i000m7:'], + [1000, '0|i00667:'], + [10000, '0|i01pq7:'], + [100000, '0|i0h5a7:'], + [1000000, '0|i4rgu7:'] + ])('produces next rank of reasonable length for %p generations', (count, expected) => { + let rank = '0|hzzzzz:' + for (let i = 0; i < count; i++) { + rank = makeRank(rank, undefined) + } + expect(rank).toBe(expected) + }) +}) diff --git a/foundations/core/packages/rank/src/index.ts b/foundations/core/packages/rank/src/index.ts new file mode 100644 index 0000000000..f6894a9426 --- /dev/null +++ b/foundations/core/packages/rank/src/index.ts @@ -0,0 +1,17 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './types' +export * from './utils' diff --git a/foundations/core/packages/rank/src/types.ts b/foundations/core/packages/rank/src/types.ts new file mode 100644 index 0000000000..6a7bb43685 --- /dev/null +++ b/foundations/core/packages/rank/src/types.ts @@ -0,0 +1,16 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export { type Rank } from '@hcengineering/core' diff --git a/foundations/core/packages/rank/src/utils.ts b/foundations/core/packages/rank/src/utils.ts new file mode 100644 index 0000000000..4f43e8925b --- /dev/null +++ b/foundations/core/packages/rank/src/utils.ts @@ -0,0 +1,62 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket' +import type { Rank } from './types' +import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank' + +/** @public */ +export function genRanks (count: number): Rank[] { + const sys = new LexoNumeralSystem36() + const base = 36 + const max = base ** 6 + const gap = LexoDecimal.parse(Math.trunc(max / (count + 2)).toString(base), sys) + let cur = LexoDecimal.parse('0', sys) + const res: string[] = [] + for (let i = 0; i < count; i++) { + cur = cur.add(gap) + res.push(new LexoRank(LexoRankBucket.BUCKET_0, cur).toString()) + } + return res +} + +/** @public */ +export function makeRank (prev: Rank | undefined, next: Rank | undefined): Rank { + try { + if (prev != null && prev.trim() === '') { + prev = undefined + } + if (next != null && next.trim() === '') { + next = undefined + } + if (prev !== undefined && next !== undefined) { + const prevLexoRank = LexoRank.parse(prev) + const nextLexoRank = LexoRank.parse(next) + return prevLexoRank.equals(nextLexoRank) + ? prevLexoRank.genNext().toString() + : prevLexoRank.between(nextLexoRank).toString() + } else if (prev !== undefined) { + const prevLexoRank = LexoRank.parse(prev) + return prevLexoRank.genNext().toString() + } else if (next !== undefined) { + const nextLexoRank = LexoRank.parse(next) + return nextLexoRank.genPrev().toString() + } else { + return LexoRank.middle().toString() + } + } catch (err: any) { + throw new Error(`Failed to make rank: ${prev} ${next} ${err.message}`) + } +} diff --git a/foundations/core/packages/rank/tsconfig.json b/foundations/core/packages/rank/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/rank/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/retry/.eslintrc.js b/foundations/core/packages/retry/.eslintrc.js new file mode 100644 index 0000000000..ce90fb9646 --- /dev/null +++ b/foundations/core/packages/retry/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/retry/.npmignore b/foundations/core/packages/retry/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/retry/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/retry/CHANGELOG.json b/foundations/core/packages/retry/CHANGELOG.json new file mode 100644 index 0000000000..621fc9eb8d --- /dev/null +++ b/foundations/core/packages/retry/CHANGELOG.json @@ -0,0 +1,29 @@ +{ + "name": "@hcengineering/retry", + "entries": [ + { + "version": "0.7.5", + "tag": "@hcengineering/retry_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/retry_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/retry/CHANGELOG.md b/foundations/core/packages/retry/CHANGELOG.md new file mode 100644 index 0000000000..7fa2f71d0f --- /dev/null +++ b/foundations/core/packages/retry/CHANGELOG.md @@ -0,0 +1,18 @@ +# Change Log - @hcengineering/retry + +This log was last generated on Tue, 14 Oct 2025 04:58:17 GMT and should not be manually modified. + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + diff --git a/foundations/core/packages/retry/config/rig.json b/foundations/core/packages/retry/config/rig.json new file mode 100644 index 0000000000..78cc5a1733 --- /dev/null +++ b/foundations/core/packages/retry/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "node" +} diff --git a/foundations/core/packages/retry/jest.config.js b/foundations/core/packages/retry/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/retry/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/retry/package.json b/foundations/core/packages/retry/package.json new file mode 100644 index 0000000000..5a94f3c87d --- /dev/null +++ b/foundations/core/packages/retry/package.json @@ -0,0 +1,57 @@ +{ + "name": "@hcengineering/retry", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Copyright © Hardcore Engineering Inc.", + "template": "@hcengineering/node-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --forceExit --coverage", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --forceExit --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "@types/node": "^22.18.1", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "eslint-plugin-svelte": "^2.35.1" + }, + "files": [ + "lib/**/*", + "!lib/**/__test__/**", + "types/**/*", + "!types/**/__test__/**", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + } +} diff --git a/foundations/core/packages/retry/readme.md b/foundations/core/packages/retry/readme.md new file mode 100644 index 0000000000..55eda694b5 --- /dev/null +++ b/foundations/core/packages/retry/readme.md @@ -0,0 +1,252 @@ +# Retry Utility + +A comprehensive TypeScript utility for handling transient failures with exponential backoff, jitter, and customizable retry conditions. + +## Features + +- ✅ **Exponential backoff** with configurable parameters +- ✅ **Jitter support** to prevent thundering herd problems +- ✅ **Customizable retry conditions** to control which errors should be retried +- ✅ **TypeScript decorators** for clean, declarative retry logic +- ✅ **Function wrappers** for retrofitting existing code with retry capabilities +- ✅ **Comprehensive logging** of retry attempts and failures + +## Usage + +### Basic Usage + +Wrap any async operation with the `withRetry` function: + +```typescript +import { withRetry } from '@hcengineering/retry' + +async function fetchData() { + const data = await withRetry( + async () => { + // Your async operation that might fail transiently + return await api.getData() + }, + { maxRetries: 3 } + ) + return data +} +``` + +### Using Decorators + +For class methods, you can use the `@Retryable` decorator for clean, declarative retry logic: + +```typescript +import { Retryable } from '@hcengineering/retry' + +class UserService { + @Retryable({ maxRetries: 5 }) + async getUserProfile(userId: string): Promise { + // This method will automatically retry on failure + return await this.api.fetchUserProfile(userId) + } +} +``` + +### Delay Strategies + +The package provides several delay strategies to control the timing between retry attempts: + +Exponential Backoff +Increases the delay exponentially between retries, which is ideal for backing off from overloaded services: + +```typescript +import { withRetry, DelayStrategyFactory } from '@hcengineering/retry' + +await withRetry( + async () => await api.getData(), + { + maxRetries: 5, + delayStrategy: DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 100, // Start with 100ms + maxDelayMs: 10000, // Cap at 10 seconds + backoffFactor: 2, // Double the delay each time (100, 200, 400, 800, 1600) + jitter: 0.2 // Add ±20% randomness + }) + } +) +``` + +Fixed Delay +Uses the same delay for all retry attempts, useful when retrying after a fixed cooldown period: +```typescript +import { withRetry, DelayStrategyFactory } from '@hcengineering/retry' + +await withRetry( + async () => await api.getData(), + { + maxRetries: 3, + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 1000, // Always wait 1 second between retries + jitter: 0.1 // Optional: add ±10% randomness + }) + } +) +``` +Fibonacci Delay +Uses the Fibonacci sequence to calculate delays, providing a more moderate growth rate than exponential backoff: +```typescript +import { withRetry, DelayStrategyFactory } from '@hcengineering/retry' + +await withRetry( + async () => await api.getData(), + { + maxRetries: 6, + delayStrategy: DelayStrategyFactory.fibonacci({ + baseDelayMs: 100, // Base unit for Fibonacci sequence + maxDelayMs: 10000, // Maximum delay cap + jitter: 0.2 // Add ±20% randomness + }) + } +) +// Delays follow Fibonacci sequence: 100ms, 200ms, 300ms, 500ms, 800ms, ... +``` + +### Custom Retry Conditions + +You can specify which errors should trigger retries: + +```typescript +import { withRetry, retryNetworkErrors } from '@platform/utils/retry' + +async function fetchData() { + return await withRetry( + async () => await api.getData(), + { + // Only retry network-related errors + isRetryable: retryNetworkErrors, + maxRetries: 5 + } + ) +} +``` + +Create your own custom retry condition: + +```typescript +import { type IsRetryable } from '@platform/utils/retry' + +// Custom retry condition +const retryDatabaseErrors: IsRetryable = (error: unknown): boolean => { + if (error instanceof DatabaseError) { + // Only retry specific database errors + return error.code === 'CONNECTION_LOST' || + error.code === 'DEADLOCK' || + error.code === 'TIMEOUT' + } + return false +} + +// Use it +await withRetry( + async () => await db.query('SELECT * FROM users'), + { isRetryable: retryDatabaseErrors } +) +``` + +## API Reference + +### `withRetry(operation, options?, operationName?): Promise` + +Executes an async operation with retry logic. + +- `operation`: `() => Promise` - The async operation to execute +- `options`: `Partial` - Retry configuration (optional) +- `operationName`: `string` - Name for logging (optional) +- Returns: `Promise` - The result of the operation + +### `createRetryableFunction(fn, options?, operationName?): T` + +Creates a retryable function from an existing function. + +- `fn`: `T extends (...args: any[]) => Promise` - The function to make retryable +- `options`: `Partial` - Retry configuration (optional) +- `operationName`: `string` - Name for logging (optional) +- Returns: `T` - A wrapped function with retry logic + +### `@Retryable(options?)` + +Method decorator for adding retry functionality to class methods. + +- `options`: `Partial` - Retry configuration (optional) + +### RetryOptions + +Configuration options for the retry mechanism: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `initialDelayMs` | `number` | `1000` | Initial delay between retries in milliseconds | +| `maxDelayMs` | `number` | `30000` | Maximum delay between retries in milliseconds | +| `maxRetries` | `number` | `5` | Maximum number of retry attempts | +| `backoffFactor` | `number` | `1.5` | Backoff factor for exponential delay increase | +| `jitter` | `number` | `0.2` | Jitter factor (0-1) to add randomness to delay times | +| `isRetryable` | `IsRetryable` | `retryAllErrors` | Function to determine if an error is retriable | +| `logger` | `Logger` | `defaultLogger` | Logger to use | + +### Retry Condition Functions + +| Function | Description | +|----------|-------------| +| `retryAllErrors` | Retry on any error (default) | +| `retryNetworkErrors` | Retry only on network-related errors | + +## Examples + +### Basic Retry with Custom Options + +```typescript +import { withRetry } from '@platform/utils/retry' + +async function fetchDataWithRetry() { + return await withRetry( + async () => { + const response = await fetch('https://api.example.com/data') + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`) + } + return await response.json() + }, + { + initialDelayMs: 300, // Start with 300ms delay + maxDelayMs: 10000, // Max delay of 10 seconds + maxRetries: 4, // Try up to 4 times (1 initial + 3 retries) + backoffFactor: 2, // Double the delay each time + jitter: 0.25 // Add 25% randomness to delay + }, + 'fetchApiData' // Name for logging + ) +} +``` + +### Class with Multiple Retryable Methods + +```typescript +import { Retryable, retryNetworkErrors } from '@platform/utils/retry' + +class DataService { + @Retryable({ + maxRetries: 3, + initialDelayMs: 200 + }) + async fetchUsers(): Promise { + // Will retry up to 3 times with initial 200ms delay + return await this.api.getUsers() + } + + @Retryable({ + maxRetries: 5, + initialDelayMs: 1000, + isRetryable: retryNetworkErrors + }) + async uploadFile(file: File): Promise { + // Will retry up to 5 times, but only for network errors + return await this.api.uploadFile(file) + } +} +``` diff --git a/foundations/core/packages/retry/src/__test__/decorator.test.ts b/foundations/core/packages/retry/src/__test__/decorator.test.ts new file mode 100644 index 0000000000..557661d011 --- /dev/null +++ b/foundations/core/packages/retry/src/__test__/decorator.test.ts @@ -0,0 +1,395 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { DelayStrategyFactory } from '../delay' +import { Retryable } from '../decorator' +import { type RetryOptions } from '../retry' +import { retryAllErrors } from '../retryable' + +// Instead of mocking withRetry, we'll mock setTimeout to avoid waiting in tests +jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => { + fn() + return 1 as any +}) + +describe('Retryable decorator', () => { + const mockLogger = { + warn: jest.fn(), + error: jest.fn(), + info: jest.fn() + } + + // Update the mock options to use delay strategy + const mockOptions: Partial = { + maxRetries: 3, + delayStrategy: DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 10, + maxDelayMs: 100, + backoffFactor: 2 + }), + isRetryable: retryAllErrors, + logger: mockLogger + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should retry failed operations', async () => { + // Create a test class with decorated method that fails initially then succeeds + const error = new Error('First attempt failed') + class TestService { + callCount = 0 + + @Retryable(mockOptions) + async testMethod (param1: string, param2: number): Promise { + this.callCount++ + if (this.callCount === 1) { + throw error + } + return `${param1}-${param2}` + } + } + + const service = new TestService() + const result = await service.testMethod('test', 123) + + // Check results + expect(result).toBe('test-123') + expect(service.callCount).toBe(2) // Called once, failed, then succeeded on retry + + // Check logs + expect(mockLogger.warn).toHaveBeenCalledTimes(1) + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('testMethod failed'), + expect.objectContaining({ + error, + attempt: 1 + }) + ) + expect(mockLogger.error).not.toHaveBeenCalled() + }) + + it('should work with default options', async () => { + class TestService { + callCount = 0 + + @Retryable() + async testMethod (): Promise { + this.callCount++ + if (this.callCount === 1) { + throw new Error('network error') + } + return 'success' + } + } + + const service = new TestService() + const result = await service.testMethod() + + expect(result).toBe('success') + expect(service.callCount).toBe(2) // Should have retried once + }) + + it('should preserve class instance context (this)', async () => { + class TestService { + private counter = 0 + + @Retryable(mockOptions) + async incrementAndGet (): Promise { + if (this.counter === 0) { + this.counter++ + throw new Error('network error') + } + this.counter++ + return this.counter + } + + getCounter (): number { + return this.counter + } + } + + const service = new TestService() + const result = await service.incrementAndGet() + + // Check that the class context was preserved across retries + expect(result).toBe(2) + expect(service.getCounter()).toBe(2) // Incremented once per attempt + }) + + it('should throw after max retries are exhausted', async () => { + class TestService { + @Retryable({ + maxRetries: 2, // Only try twice total (initial + 1 retry) + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 10 + }), + logger: mockLogger, + isRetryable: retryAllErrors + }) + async alwaysFailingMethod (): Promise { + throw new Error('Persistent failure') + } + } + + const service = new TestService() + await expect(service.alwaysFailingMethod()).rejects.toThrow('Persistent failure') + + // Should have tried twice in total (initial + 1 retry) + expect(mockLogger.warn).toHaveBeenCalledTimes(1) + expect(mockLogger.error).toHaveBeenCalledTimes(1) + expect(mockLogger.error).toHaveBeenCalledWith( + expect.stringContaining('alwaysFailingMethod failed after 2 attempts'), + expect.any(Object) + ) + }) + + it('should handle async methods correctly', async () => { + let resolutionCount = 0 + + // Create a class with an async method that fails then resolves + class TestService { + @Retryable(mockOptions) + async delayedMethod (): Promise { + return await new Promise((resolve, reject) => { + resolutionCount++ + if (resolutionCount === 1) { + reject(new Error('Delayed error')) + } else { + resolve('delayed success') + } + }) + } + } + + const service = new TestService() + const result = await service.delayedMethod() + + expect(result).toBe('delayed success') + expect(resolutionCount).toBe(2) + expect(mockLogger.warn).toHaveBeenCalledTimes(1) + }) + + it('should retry according to the specified retry count', async () => { + let callCount = 0 + + class TestService { + @Retryable({ + maxRetries: 5, // Should try up to 5 times total + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 10 + }), + logger: mockLogger, + isRetryable: retryAllErrors + }) + async unstableMethod (): Promise { + callCount++ + if (callCount < 4) { + // Succeed on the 4th attempt + throw new Error(`Failure #${callCount}`) + } + return 'success after retries' + } + } + + const service = new TestService() + const result = await service.unstableMethod() + + expect(result).toBe('success after retries') + expect(callCount).toBe(4) // Initial attempt + 3 retries = 4 total calls + expect(mockLogger.warn).toHaveBeenCalledTimes(3) // Should have logged 3 warnings + }) + + it('should respect different delay strategies', async () => { + // Override the setTimeout mock to capture delay values + const delayValues: number[] = [] + jest.spyOn(global, 'setTimeout').mockImplementation((fn: any, delay: number | undefined) => { + delayValues.push(delay ?? 0) + fn() + return 1 as any + }) + + let callCount = 0 + + class TestService { + @Retryable({ + maxRetries: 4, + delayStrategy: DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 100, + maxDelayMs: 500, + backoffFactor: 2, + jitter: 0 // Disable jitter for predictable tests + }), + isRetryable: retryAllErrors + }) + async delayingMethod (): Promise { + callCount++ + if (callCount < 4) { + throw new Error(`Attempt ${callCount} failed`) + } + return 'success' + } + } + + const service = new TestService() + await service.delayingMethod() + + // Should have recorded 3 delays: initial, 2x initial, 4x initial (capped at maxDelayMs) + expect(delayValues).toHaveLength(3) + expect(delayValues[0]).toBe(100) // initial delay + expect(delayValues[1]).toBe(200) // 2x initial + expect(delayValues[2]).toBe(400) // 4x initial + }) + + it('should test various delay strategies', async () => { + // Override the setTimeout mock to capture delay values + const delayValues: number[] = [] + jest.spyOn(global, 'setTimeout').mockImplementation((fn: any, delay: number | undefined) => { + delayValues.push(delay ?? 0) + fn() + return 1 as any + }) + + // Test Fixed strategy + let callCount = 0 + class FixedTestService { + @Retryable({ + maxRetries: 3, + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 100, + jitter: 0 // Disable jitter for predictable tests + }), + isRetryable: retryAllErrors + }) + async method (): Promise { + callCount++ + if (callCount < 3) { + throw new Error(`Attempt ${callCount} failed`) + } + return 'success' + } + } + + delayValues.length = 0 // Reset captured delays + await new FixedTestService().method() + expect(delayValues).toEqual([100, 100]) // Should be constant + + // Test Fibonacci strategy + callCount = 0 + class FibonacciTestService { + @Retryable({ + maxRetries: 4, + delayStrategy: DelayStrategyFactory.fibonacci({ + baseDelayMs: 100, + maxDelayMs: 1000, + jitter: 0 // Disable jitter for predictable tests + }), + isRetryable: retryAllErrors + }) + async method (): Promise { + callCount++ + if (callCount < 4) { + throw new Error(`Attempt ${callCount} failed`) + } + return 'success' + } + } + + delayValues.length = 0 // Reset captured delays + await new FibonacciTestService().method() + // Fibonacci sequence delay pattern + expect(delayValues).toEqual([100, 200, 300]) // fib(2)=1, fib(3)=2, fib(4)=3 multiplied by baseDelayMs + }) + + it('should handle methods returning non-promises', async () => { + let callCount = 0 + + class TestService { + @Retryable(mockOptions) + nonAsyncMethod (input: string): string { + callCount++ + if (callCount === 1) { + throw new Error('Sync error') + } + return `processed-${input}` + } + } + + const service = new TestService() + // Even though the original method is not async, the decorated method returns a Promise + // eslint-disable-next-line @typescript-eslint/await-thenable + const result = await service.nonAsyncMethod('test') + + expect(result).toBe('processed-test') + expect(callCount).toBe(2) // Should have retried once + expect(mockLogger.warn).toHaveBeenCalledTimes(1) + }) + + it('should handle static methods', async () => { + let callCount = 0 + + // eslint-disable-next-line @typescript-eslint/no-extraneous-class + class TestService { + @Retryable(mockOptions) + static async staticMethod (input: string): Promise { + callCount++ + if (callCount === 1) { + throw new Error('Static method error') + } + return `static-${input}` + } + } + + const result = await TestService.staticMethod('test') + + expect(result).toBe('static-test') + expect(callCount).toBe(2) // Should have retried once + }) + + it('should respect isRetryable option', async () => { + class TestService { + callCount = 0 + + @Retryable({ + maxRetries: 3, + delayStrategy: DelayStrategyFactory.fixed({ delayMs: 10 }), + logger: mockLogger, + isRetryable: (err) => { + // Only retry errors with "retry" in the message + return err instanceof Error && err.message.includes('Please retry') + } + }) + async conditionalRetryMethod (): Promise { + this.callCount++ + if (this.callCount === 1) { + throw new Error('Please retry this') // should be retried + } + if (this.callCount === 2) { + throw new Error('Do not retry this') // should not be retried + } + return 'success' + } + } + + const service = new TestService() + // Should fail with the second error since it won't be retried + await expect(service.conditionalRetryMethod()).rejects.toThrow('Do not retry this') + + expect(service.callCount).toBe(2) // Should have called twice (original + 1 retry) + expect(mockLogger.warn).toHaveBeenCalledTimes(1) // Only first error logged as warning + expect(mockLogger.error).toHaveBeenCalledTimes(1) // Second error logged as error + }) +}) diff --git a/foundations/core/packages/retry/src/__test__/delay.test.ts b/foundations/core/packages/retry/src/__test__/delay.test.ts new file mode 100644 index 0000000000..67df348b8b --- /dev/null +++ b/foundations/core/packages/retry/src/__test__/delay.test.ts @@ -0,0 +1,296 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { DelayStrategyFactory, ExponentialBackoffStrategy, FibonacciDelayStrategy, FixedDelayStrategy } from '../delay' + +describe('Delay Strategies', () => { + // Mock Math.random to return fixed values in tests + let randomMock: jest.SpyInstance + + beforeEach(() => { + // Default mock returns 0.5 for Math.random, which creates 0 jitter effect + randomMock = jest.spyOn(Math, 'random').mockReturnValue(0.5) + }) + + afterEach(() => { + randomMock.mockRestore() + }) + + describe('FixedDelayStrategy', () => { + it('should return the same delay for all attempts without jitter', () => { + const delay = 1000 + const strategy = new FixedDelayStrategy({ delayMs: delay }) + + expect(strategy.getDelay(1)).toBe(delay) + expect(strategy.getDelay(2)).toBe(delay) + expect(strategy.getDelay(5)).toBe(delay) + expect(strategy.getDelay(10)).toBe(delay) + }) + + it('should apply jitter correctly', () => { + const delay = 1000 + const jitter = 0.2 // 20% jitter + const strategy = new FixedDelayStrategy({ delayMs: delay, jitter }) + + // Mock random to return different values + randomMock.mockReturnValueOnce(0.6) // => +0.2 * delay + randomMock.mockReturnValueOnce(0.4) // => -0.2 * delay + randomMock.mockReturnValueOnce(1.0) // => +1.0 * delay + + // With 0.6 random value, jitter effect is (0.6-0.5)*2*0.2 = +0.04 => 4% increase + expect(strategy.getDelay(1)).toBe(delay + delay * 0.2 * 0.2) + + // With 0.4 random value, jitter effect is (0.4-0.5)*2*0.2 = -0.04 => 4% decrease + expect(strategy.getDelay(2)).toBe(delay - delay * 0.2 * 0.2) + + // With 1.0 random value, jitter effect is (1.0-0.5)*2*0.2 = +0.2 => 20% increase + expect(strategy.getDelay(3)).toBe(delay + delay * 0.2 * 1.0) + }) + + it('should never return negative values even with high jitter', () => { + const delay = 100 + const jitter = 1.0 // 100% jitter, extreme case + const strategy = new FixedDelayStrategy({ delayMs: delay, jitter }) + + // Mock random to return min value (full negative jitter) + randomMock.mockReturnValue(0) // => -1.0 * delay + + // With 0 random value and 1.0 jitter, the effect would be -100%, but should be capped at 0 + expect(strategy.getDelay(1)).toBe(0) + }) + + it('should be creatable through factory method', () => { + const strategy = DelayStrategyFactory.fixed({ + delayMs: 1000, + jitter: 0.1 + }) + expect(strategy).toBeInstanceOf(FixedDelayStrategy) + expect(strategy.getDelay(1)).toBe(1000) // With 0.5 mock random, no jitter effect + }) + }) + + describe('ExponentialBackoffStrategy', () => { + it('should increase delay exponentially without jitter', () => { + const initial = 1000 + const max = 60000 + const factor = 2 + const strategy = new ExponentialBackoffStrategy({ + initialDelayMs: initial, + maxDelayMs: max, + backoffFactor: factor + }) + + expect(strategy.getDelay(1)).toBe(initial) // 1000 + expect(strategy.getDelay(2)).toBe(initial * factor) // 2000 + expect(strategy.getDelay(3)).toBe(initial * Math.pow(factor, 2)) // 4000 + expect(strategy.getDelay(4)).toBe(initial * Math.pow(factor, 3)) // 8000 + }) + + it('should respect maximum delay', () => { + const strategy = new ExponentialBackoffStrategy({ + initialDelayMs: 1000, + maxDelayMs: 5000, + backoffFactor: 2 + }) + + expect(strategy.getDelay(1)).toBe(1000) + expect(strategy.getDelay(2)).toBe(2000) + expect(strategy.getDelay(3)).toBe(4000) + // Should be capped at 5000 + expect(strategy.getDelay(4)).toBe(5000) + expect(strategy.getDelay(5)).toBe(5000) + }) + + it('should apply jitter correctly', () => { + const strategy = new ExponentialBackoffStrategy({ + initialDelayMs: 1000, + maxDelayMs: 60000, + backoffFactor: 2, + jitter: 0.2 + }) + + // Mock random to return different values + randomMock.mockReturnValueOnce(0.6) // => +0.2 * delay + randomMock.mockReturnValueOnce(0.4) // => -0.2 * delay + + // First attempt: 1000ms with jitter +4% + expect(strategy.getDelay(1)).toBe(1000 + 1000 * 0.2 * 0.2) + + // Second attempt: 2000ms with jitter -4% + expect(strategy.getDelay(2)).toBe(2000 - 2000 * 0.2 * 0.2) + }) + + it('should be creatable through factory method', () => { + const strategy = DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 1000, + maxDelayMs: 60000, + backoffFactor: 2, + jitter: 0.1 + }) + expect(strategy).toBeInstanceOf(ExponentialBackoffStrategy) + expect(strategy.getDelay(1)).toBe(1000) // With 0.5 mock random, no jitter effect + expect(strategy.getDelay(2)).toBe(2000) + }) + }) + + describe('FibonacciDelayStrategy', () => { + it('should follow Fibonacci sequence without jitter', () => { + const baseDelay = 100 + const maxDelay = 10000 + const strategy = new FibonacciDelayStrategy({ + baseDelayMs: baseDelay, + maxDelayMs: maxDelay + }) + + expect(strategy.getDelay(1)).toBe(baseDelay * 1) + expect(strategy.getDelay(2)).toBe(baseDelay * 2) + expect(strategy.getDelay(3)).toBe(baseDelay * 3) + expect(strategy.getDelay(4)).toBe(baseDelay * 5) + expect(strategy.getDelay(5)).toBe(baseDelay * 8) + expect(strategy.getDelay(6)).toBe(baseDelay * 13) + }) + + it('should respect maximum delay', () => { + const strategy = new FibonacciDelayStrategy({ + baseDelayMs: 100, + maxDelayMs: 500 + }) + + expect(strategy.getDelay(1)).toBe(100) + expect(strategy.getDelay(2)).toBe(200) + expect(strategy.getDelay(3)).toBe(300) + expect(strategy.getDelay(4)).toBe(500) + expect(strategy.getDelay(5)).toBe(500) // Capped at maxDelayMs + expect(strategy.getDelay(6)).toBe(500) // Capped at maxDelayMs + }) + + it('should cache Fibonacci calculations for performance', () => { + const strategy = new FibonacciDelayStrategy({ + baseDelayMs: 100, + maxDelayMs: 10000 + }) + + // Access private cache to verify it's working + const fibCache = (strategy as any).fibCache + expect(fibCache.size).toBe(2) // Initial cache has 0->0 and 1->1 + + strategy.getDelay(7) // Should calculate fib(8) = 21 + + expect(fibCache.size).toBeGreaterThan(2) + expect(fibCache.get(8)).toBe(21) + }) + + it('should apply jitter correctly', () => { + const strategy = new FibonacciDelayStrategy({ + baseDelayMs: 100, + maxDelayMs: 10000, + jitter: 0.2 + }) + + // Mock random to return different values + randomMock.mockReturnValueOnce(0.6) // => +0.2 * delay + randomMock.mockReturnValueOnce(0.4) // => -0.2 * delay + + // First attempt: 100ms (fib(2)=1 * 100) with jitter +4% + expect(strategy.getDelay(1)).toBe(100 + 100 * 0.2 * 0.2) + + // Second attempt: 100ms (fib(3)=1 * 100) with jitter -4% + expect(strategy.getDelay(2)).toBe(200 - 200 * 0.2 * 0.2) + }) + + it('should handle large Fibonacci numbers efficiently', () => { + const strategy = new FibonacciDelayStrategy({ + baseDelayMs: 1, + maxDelayMs: Number.MAX_SAFE_INTEGER + }) + + // This would be extremely slow without memoization + const start = performance.now() + const delay = strategy.getDelay(40) // fib(41) = 165580141 + const duration = performance.now() - start + + // Should be much faster than calculating naively + expect(duration).toBeLessThan(100) + expect(delay).toBe(165580141) // fib(41) * 1 + }) + + it('should be creatable through factory method', () => { + const strategy = DelayStrategyFactory.fibonacci({ + baseDelayMs: 100, + maxDelayMs: 10000, + jitter: 0.1 + }) + expect(strategy).toBeInstanceOf(FibonacciDelayStrategy) + expect(strategy.getDelay(1)).toBe(100) + expect(strategy.getDelay(2)).toBe(200) + expect(strategy.getDelay(3)).toBe(300) + expect(strategy.getDelay(4)).toBe(500) + }) + }) + + describe('DelayStrategyFactory', () => { + it('should create strategies with correct types', () => { + expect( + DelayStrategyFactory.fixed({ + delayMs: 1000 + }) + ).toBeInstanceOf(FixedDelayStrategy) + + expect( + DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 1000, + maxDelayMs: 60000, + backoffFactor: 2 + }) + ).toBeInstanceOf(ExponentialBackoffStrategy) + + expect( + DelayStrategyFactory.fibonacci({ + baseDelayMs: 100, + maxDelayMs: 10000 + }) + ).toBeInstanceOf(FibonacciDelayStrategy) + }) + + it('should pass parameters correctly to strategies', () => { + const fixed = DelayStrategyFactory.fixed({ + delayMs: 1000, + jitter: 0.1 + }) as FixedDelayStrategy + expect((fixed as any).delayMs).toBe(1000) + expect((fixed as any).jitter).toBe(0.1) + + const exponential = DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 1000, + maxDelayMs: 60000, + backoffFactor: 2.5, + jitter: 0.2 + }) as ExponentialBackoffStrategy + expect((exponential as any).initialDelayMs).toBe(1000) + expect((exponential as any).maxDelayMs).toBe(60000) + expect((exponential as any).backoffFactor).toBe(2.5) + expect((exponential as any).jitter).toBe(0.2) + + const fibonacci = DelayStrategyFactory.fibonacci({ + baseDelayMs: 100, + maxDelayMs: 10000, + jitter: 0.15 + }) as FibonacciDelayStrategy + expect((fibonacci as any).baseDelayMs).toBe(100) + expect((fibonacci as any).maxDelayMs).toBe(10000) + expect((fibonacci as any).jitter).toBe(0.15) + }) + }) +}) diff --git a/foundations/core/packages/retry/src/__test__/retry.test.ts b/foundations/core/packages/retry/src/__test__/retry.test.ts new file mode 100644 index 0000000000..7118520e46 --- /dev/null +++ b/foundations/core/packages/retry/src/__test__/retry.test.ts @@ -0,0 +1,560 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { DelayStrategyFactory } from '../delay' +import { withRetry, createRetryableFunction, type RetryOptions } from '../retry' +import { type IsRetryable, retryAllErrors } from '../retryable' + +// Mock the sleep function to speed up tests +jest.mock('../delay', () => { + const originalModule = jest.requireActual('../delay') + return { + ...originalModule, + // Override the internal sleep function to resolve immediately + sleep: jest.fn().mockImplementation(() => Promise.resolve()) + } +}) + +describe('withRetry', () => { + // Create a mock logger to capture logs + const mockLogger = { + warn: jest.fn(), + error: jest.fn(), + info: jest.fn() + } + + // Use the new delayStrategy option + const mockOptions: Partial = { + maxRetries: 3, + delayStrategy: DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 10, + maxDelayMs: 100, + backoffFactor: 2, + jitter: 0 + }), + logger: mockLogger, + isRetryable: retryAllErrors + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return the result when operation succeeds on first try', async () => { + const mockOperation = jest.fn().mockResolvedValue('success') + + const result = await withRetry(mockOperation, mockOptions) + + expect(result).toBe('success') + expect(mockOperation).toHaveBeenCalledTimes(1) + expect(mockLogger.warn).not.toHaveBeenCalled() + expect(mockLogger.error).not.toHaveBeenCalled() + }) + + it('should retry when operation fails and eventually succeed', async () => { + const mockOperation = jest + .fn() + .mockRejectedValueOnce(new Error('first failure')) + .mockRejectedValueOnce(new Error('second failure')) + .mockResolvedValueOnce('success after retries') + + const result = await withRetry(mockOperation, mockOptions) + + expect(result).toBe('success after retries') + expect(mockOperation).toHaveBeenCalledTimes(3) + expect(mockLogger.warn).toHaveBeenCalledTimes(2) + expect(mockLogger.error).not.toHaveBeenCalled() + }) + + it('should throw an error after maximum retries are exhausted', async () => { + const mockError = new Error('persistent failure') + const mockOperation = jest.fn().mockRejectedValue(mockError) + + await expect(withRetry(mockOperation, mockOptions)).rejects.toThrow('persistent failure') + + expect(mockOperation).toHaveBeenCalledTimes(mockOptions.maxRetries ?? -1) + expect(mockLogger.error).toHaveBeenCalledTimes(1) + }) + + it('should use default options when none are provided', async () => { + const mockOperation = jest.fn().mockResolvedValue('success') + + const result = await withRetry(mockOperation) + + expect(result).toBe('success') + expect(mockOperation).toHaveBeenCalledTimes(1) + }) + + it('should use provided operation name in log messages', async () => { + const mockOperation = jest.fn().mockRejectedValueOnce(new Error('failure')).mockResolvedValueOnce('success') + + await withRetry(mockOperation, mockOptions, 'custom-operation') + + expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('custom-operation failed'), expect.any(Object)) + }) + + it('should apply jitter to delay calculation', async () => { + const mockOperation = jest.fn().mockRejectedValueOnce(new Error('failure')).mockResolvedValueOnce('success') + + // Use Math.random mock to make jitter predictable + const mockRandom = jest.spyOn(Math, 'random').mockReturnValue(0.5) + + // Create options with jitter enabled + const jitterOptions = { + ...mockOptions, + delayStrategy: DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 10, + maxDelayMs: 100, + backoffFactor: 2, + jitter: 0.2 + }) + } + + await withRetry(mockOperation, jitterOptions) + + // With Math.random = 0.5, jitter should be 0 + // (since 0.5 * 2 - 1 = 0) + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ delayMs: 10 }) // Should still be the initial delay + ) + + mockRandom.mockRestore() + }) + + it('should respect maximum delay', async () => { + const mockOperation = jest + .fn() + .mockRejectedValueOnce(new Error('failure 1')) + .mockRejectedValueOnce(new Error('failure 2')) + .mockRejectedValueOnce(new Error('failure 3')) + .mockResolvedValueOnce('success') + + // Use high backoff factor to test maximum delay cap + const maxDelayOptions = { + maxRetries: 4, + delayStrategy: DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 50, + maxDelayMs: 1000, + backoffFactor: 10 // Would normally go 50 -> 500 -> 5000, but should cap at 1000 + }), + isRetryable: retryAllErrors, + logger: mockLogger + } + + await withRetry(mockOperation, maxDelayOptions) + + // Check that delays are correctly calculated and capped + expect(mockLogger.warn).toHaveBeenNthCalledWith( + 1, + expect.any(String), + expect.objectContaining({ delayMs: expect.any(Number) }) + ) + + // Second retry delay + expect(mockLogger.warn).toHaveBeenNthCalledWith( + 2, + expect.any(String), + expect.objectContaining({ delayMs: 500 }) // 50 * 10 = 500 + ) + + // Third retry delay (should be capped) + expect(mockLogger.warn).toHaveBeenNthCalledWith( + 3, + expect.any(String), + expect.objectContaining({ delayMs: 1000 }) // 500 * 10 = 5000, capped at 1000 + ) + + // Function should have been called 4 times total + expect(mockOperation).toHaveBeenCalledTimes(4) + }) + + it('should work with different delay strategies', async () => { + // Test with fixed delay + const fixedDelayOperation = jest + .fn() + .mockRejectedValueOnce(new Error('failure 1')) + .mockRejectedValueOnce(new Error('failure 2')) + .mockResolvedValueOnce('success') + + const fixedDelayOptions = { + maxRetries: 3, + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 200, + jitter: 0 + }), + isRetryable: retryAllErrors, + logger: mockLogger + } + + await withRetry(fixedDelayOperation, fixedDelayOptions) + + // Both retries should have the same delay + expect(mockLogger.warn).toHaveBeenNthCalledWith(1, expect.any(String), expect.objectContaining({ delayMs: 200 })) + expect(mockLogger.warn).toHaveBeenNthCalledWith(2, expect.any(String), expect.objectContaining({ delayMs: 200 })) + + // Clear mocks for next test + jest.clearAllMocks() + + // Test with Fibonacci delay + const fibonacciDelayOperation = jest + .fn() + .mockRejectedValueOnce(new Error('failure 1')) + .mockRejectedValueOnce(new Error('failure 2')) + .mockResolvedValueOnce('success') + + const fibonacciDelayOptions = { + maxRetries: 3, + delayStrategy: DelayStrategyFactory.fibonacci({ + baseDelayMs: 100, + maxDelayMs: 10000, + jitter: 0 + }), + isRetryable: retryAllErrors, + logger: mockLogger + } + + await withRetry(fibonacciDelayOperation, fibonacciDelayOptions) + + // Delays should follow Fibonacci sequence + expect(mockLogger.warn).toHaveBeenNthCalledWith( + 1, + expect.any(String), + expect.objectContaining({ delayMs: 100 }) // fib(2) = 1 * 100 + ) + expect(mockLogger.warn).toHaveBeenNthCalledWith( + 2, + expect.any(String), + expect.objectContaining({ delayMs: 200 }) // fib(3) = 2 * 100 + ) + }) +}) + +describe('createRetryableFunction', () => { + const mockLogger = { + warn: jest.fn(), + error: jest.fn(), + info: jest.fn() + } + + const mockOptions: Partial = { + maxRetries: 2, + delayStrategy: DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 10, + maxDelayMs: 100, + backoffFactor: 2 + }), + isRetryable: retryAllErrors, + logger: mockLogger + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should create a function that applies retry logic', async () => { + const mockFn = jest.fn().mockRejectedValueOnce(new Error('first failure')).mockResolvedValueOnce('success') + + const retryableFn = createRetryableFunction(mockFn, mockOptions) + + const result = await retryableFn('arg1', 123) + + expect(result).toBe('success') + expect(mockFn).toHaveBeenCalledTimes(2) + expect(mockFn).toHaveBeenCalledWith('arg1', 123) + expect(mockLogger.warn).toHaveBeenCalledTimes(1) + }) + + it('should pass through function parameters correctly', async () => { + const mockFn = jest.fn().mockResolvedValue('success') + + const retryableFn = createRetryableFunction(mockFn, mockOptions) + + await retryableFn('arg1', 123, { complex: true }) + + expect(mockFn).toHaveBeenCalledWith('arg1', 123, { complex: true }) + }) + + it('should use custom operation name in logs', async () => { + const mockFn = jest.fn().mockRejectedValueOnce(new Error('failure')).mockResolvedValueOnce('success') + + const retryableFn = createRetryableFunction(mockFn, mockOptions, 'custom-function') + + await retryableFn() + + expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('custom-function failed'), expect.any(Object)) + }) + + it('should propagate the final error if all retries fail', async () => { + const mockError = new Error('network error') + const mockFn = jest.fn().mockRejectedValue(mockError) + + const retryableFn = createRetryableFunction(mockFn, mockOptions) + + await expect(retryableFn()).rejects.toThrow('network error') + + expect(mockOptions.maxRetries).toBeDefined() + expect(mockFn).toHaveBeenCalledTimes(mockOptions.maxRetries ?? -1) + }) +}) + +// Test with a decorated class +describe('Using retry in class methods', () => { + class TestService { + counter = 0 + + async unstableFunction (): Promise { + this.counter++ + if (this.counter < 3) { + throw new Error(`network error ${this.counter}`) + } + return 'success' + } + } + + it('should work with instance methods', async () => { + const service = new TestService() + + // Create a retryable version of the method that's bound to the service + const retryableMethod = createRetryableFunction(service.unstableFunction.bind(service), { + maxRetries: 3, + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 10 + }) + }) + + const result = await retryableMethod() + + expect(result).toBe('success') + expect(service.counter).toBe(3) + }) +}) + +describe('withRetry with isRetryable option', () => { + // Create a mock logger to capture logs + const mockLogger = { + warn: jest.fn(), + error: jest.fn(), + info: jest.fn() + } + + beforeEach(() => { + jest.clearAllMocks() + // Mock the sleep function to speed up tests + jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => { + fn() + return 1 as any + }) + }) + + it('should retry errors that are marked as retriable', async () => { + // Custom isRetryable function that only retries certain errors + const customRetriable: IsRetryable = (err: any) => { + return err.message.includes('retriable') + } + + const mockOperation = jest + .fn() + .mockRejectedValueOnce(new Error('This is a retriable error')) + .mockRejectedValueOnce(new Error('This is a retriable error again')) + .mockResolvedValueOnce('success') + + const result = await withRetry(mockOperation, { + maxRetries: 5, + logger: mockLogger, + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 10 + }), + isRetryable: customRetriable + }) + + expect(result).toBe('success') + expect(mockOperation).toHaveBeenCalledTimes(3) + expect(mockLogger.warn).toHaveBeenCalledTimes(2) + expect(mockLogger.error).not.toHaveBeenCalled() + }) + + it('should not retry errors that are not marked as retriable', async () => { + // Custom isRetryable function that never retries + const neverRetry: IsRetryable = (_err: any) => { + return false + } + + const mockOperation = jest + .fn() + .mockRejectedValueOnce(new Error('This error should not be retried')) + .mockResolvedValueOnce('success') + + await expect( + withRetry(mockOperation, { + maxRetries: 5, + logger: mockLogger, + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 10 + }), + isRetryable: neverRetry + }) + ).rejects.toThrow('This error should not be retried') + + expect(mockOperation).toHaveBeenCalledTimes(1) + expect(mockLogger.warn).not.toHaveBeenCalled() + expect(mockLogger.error).toHaveBeenCalledWith( + expect.stringContaining('failed with non-retriable error'), + expect.any(Object) + ) + }) + + it('should have different behavior for different error types', async () => { + // Custom isRetryable function that retries only NetworkErrors + const retryOnlyNetworkErrors: IsRetryable = (err: any) => { + return err.name === 'NetworkError' + } + + // Create different error types + const networkError = new Error('Network failed') + networkError.name = 'NetworkError' + + const validationError = new Error('Validation failed') + validationError.name = 'ValidationError' + + // First test with network error - should be retried + const mockNetworkOp = jest.fn().mockRejectedValueOnce(networkError).mockResolvedValueOnce('network success') + + const result1 = await withRetry(mockNetworkOp, { + maxRetries: 3, + logger: mockLogger, + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 10 + }), + isRetryable: retryOnlyNetworkErrors + }) + + expect(result1).toBe('network success') + expect(mockNetworkOp).toHaveBeenCalledTimes(2) + + // Reset mocks + jest.clearAllMocks() + + // Then test with validation error - should not be retried + const mockValidationOp = jest.fn().mockRejectedValueOnce(validationError) + + await expect( + withRetry(mockValidationOp, { + maxRetries: 3, + logger: mockLogger, + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 10 + }), + isRetryable: retryOnlyNetworkErrors + }) + ).rejects.toThrow('Validation failed') + + expect(mockValidationOp).toHaveBeenCalledTimes(1) + expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('non-retriable error'), expect.any(Object)) + }) + + it('should use the default retryNetworkErrors if isRetryable is not provided', async () => { + // All errors should be retried by default + const mockOperation = jest + .fn() + .mockRejectedValueOnce(new Error('unreachable')) + .mockRejectedValueOnce(new Error('ECONNREFUSED')) + .mockResolvedValueOnce('success') + + const result = await withRetry(mockOperation, { + maxRetries: 5, + logger: mockLogger, + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 10 + }) + // isRetryable not provided, should use default + }) + + expect(result).toBe('success') + expect(mockOperation).toHaveBeenCalledTimes(3) + expect(mockLogger.warn).toHaveBeenCalledTimes(2) + }) + + it('should pass the error to isRetryable function', async () => { + // Mock isRetryable function to track calls + const mockisRetryable = jest.fn().mockReturnValue(true) + + const testError = new Error('test error') + const mockOperation = jest.fn().mockRejectedValueOnce(testError).mockResolvedValueOnce('success') + + await withRetry(mockOperation, { + maxRetries: 3, + logger: mockLogger, + delayStrategy: DelayStrategyFactory.fixed({ + delayMs: 10 + }), + isRetryable: mockisRetryable + }) + + // Verify isRetryable was called with the actual error + expect(mockisRetryable).toHaveBeenCalledTimes(1) + expect(mockisRetryable).toHaveBeenCalledWith(testError) + }) +}) + +describe('createRetryableFunction with isRetryable', () => { + const mockLogger = { + warn: jest.fn(), + error: jest.fn(), + info: jest.fn() + } + + beforeEach(() => { + jest.clearAllMocks() + // Mock the sleep function to speed up tests + jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => { + fn() + return 1 as any + }) + }) + + it('should respect isRetryable when applied to a function', async () => { + // Function to wrap + const unstableFunction = jest + .fn() + .mockRejectedValueOnce(new Error('retriable network error')) + .mockRejectedValueOnce(new Error('retriable network error')) + .mockResolvedValueOnce('success') + + // Custom isRetryable that only retries network errors + const customRetriable: IsRetryable = (err: any) => { + return err.message.includes('network') + } + + // Create retryable version with custom isRetryable + const retryableFunction = createRetryableFunction( + unstableFunction, + { + maxRetries: 3, + logger: mockLogger, + delayStrategy: DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 10, + maxDelayMs: 100, + backoffFactor: 2 + }), + isRetryable: customRetriable + }, + 'custom-operation' + ) + + const result = await retryableFunction() + expect(result).toBe('success') + }) +}) diff --git a/foundations/core/packages/retry/src/__test__/retryable.test.ts b/foundations/core/packages/retry/src/__test__/retryable.test.ts new file mode 100644 index 0000000000..8814cc8008 --- /dev/null +++ b/foundations/core/packages/retry/src/__test__/retryable.test.ts @@ -0,0 +1,158 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { retryAllErrors, retryNetworkErrors } from '../retryable' + +describe('retryAllErrors', () => { + it('should return true for any error', () => { + expect(retryAllErrors(new Error('any error'))).toBe(true) + expect(retryAllErrors(new TypeError('type error'))).toBe(true) + expect(retryAllErrors(null)).toBe(true) + expect(retryAllErrors(undefined)).toBe(true) + expect(retryAllErrors({ custom: 'error object' })).toBe(true) + expect(retryAllErrors('error string')).toBe(true) + }) +}) + +describe('retryNetworkErrors', () => { + it('should return false for null or undefined', () => { + expect(retryNetworkErrors(null)).toBe(false) + expect(retryNetworkErrors(undefined)).toBe(false) + }) + + it('should return true for errors with network-related names', () => { + const networkErrorNames = [ + 'NetworkError', + 'FetchError', + 'AbortError', + 'TimeoutError', + 'ConnectionError', + 'ConnectionRefusedError', + 'ETIMEDOUT', + 'ECONNREFUSED', + 'ECONNRESET', + 'ENOTFOUND', + 'EAI_AGAIN' + ] + + networkErrorNames.forEach((name) => { + // Create an error with the specified name + const error = new Error('Test error') + error.name = name + expect(retryNetworkErrors(error)).toBe(true) + }) + }) + + it('should return true for errors with network-related message patterns', () => { + const networkErrorMessages = [ + 'Network error occurred', + 'Connection timed out', + 'Connection refused', + 'Connection reset', + 'Socket hang up', + 'DNS lookup failed', + 'getaddrinfo ENOTFOUND api.example.com', + 'connect ECONNREFUSED 127.0.0.1:8080', + 'read ECONNRESET', + 'connect ETIMEDOUT 192.168.1.1:443', + 'getaddrinfo EAI_AGAIN myserver.local' + ] + + networkErrorMessages.forEach((message) => { + expect(retryNetworkErrors(new Error(message))).toBe(true) + }) + }) + + it('should return false for non-network related errors', () => { + const nonNetworkErrors = [ + new Error('Invalid input'), + new TypeError('Cannot read property of undefined'), + new RangeError('Value out of range'), + new SyntaxError('Unexpected token'), + new Error('File not found'), + new Error('Permission denied'), + new Error('Invalid state') + ] + + nonNetworkErrors.forEach((error) => { + expect(retryNetworkErrors(error)).toBe(false) + }) + }) + + it('should return true for errors with server error status codes (5xx)', () => { + const serverErrors = [ + createErrorWithStatus(500), + createErrorWithStatus(501), + createErrorWithStatus(502), + createErrorWithStatus(503), + createErrorWithStatus(504), + createErrorWithStatus(599) + ] + + serverErrors.forEach((error) => { + expect(retryNetworkErrors(error)).toBe(true) + }) + }) + + it('should return true for specific client error status codes', () => { + const retriableClientErrors = [ + createErrorWithStatus(408), // Request Timeout + createErrorWithStatus(423), // Locked + createErrorWithStatus(425), // Too Early + createErrorWithStatus(429), // Too Many Requests + createErrorWithStatus(449) // Retry With + ] + + retriableClientErrors.forEach((error) => { + expect(retryNetworkErrors(error)).toBe(true) + }) + }) + + it('should return false for non-retriable client error status codes', () => { + const nonRetriableClientErrors = [ + createErrorWithStatus(400), // Bad Request + createErrorWithStatus(401), // Unauthorized + createErrorWithStatus(403), // Forbidden + createErrorWithStatus(404), // Not Found + createErrorWithStatus(422) // Unprocessable Entity + ] + + nonRetriableClientErrors.forEach((error) => { + expect(retryNetworkErrors(error)).toBe(false) + }) + }) + + it('should return false for non-Error objects without network-related properties', () => { + const nonNetworkErrorObjects = [ + { code: 'INVALID_INPUT' }, + { code: 'AUTH_FAILED' }, + { message: 'Invalid credentials' }, + { error: 'Not found' } + ] + + nonNetworkErrorObjects.forEach((errorObj) => { + expect(retryNetworkErrors(errorObj)).toBe(false) + }) + }) +}) + +/** + * Helper function to create an Error object with a status property + */ +function createErrorWithStatus (status: number): Error { + const error: any = new Error(`HTTP Error ${status}`) + error.status = status + return error +} diff --git a/foundations/core/packages/retry/src/decorator.ts b/foundations/core/packages/retry/src/decorator.ts new file mode 100644 index 0000000000..cb6080e0ae --- /dev/null +++ b/foundations/core/packages/retry/src/decorator.ts @@ -0,0 +1,35 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { type RetryOptions, DEFAULT_RETRY_OPTIONS, withRetry } from './retry' + +/** + * Method decorator for adding retry functionality to class methods + * + * @param options - Retry configuration options + * @param operationName - Name of the operation for logging (defaults to method name) + * @returns Method decorator + */ +export function Retryable (options: Partial = DEFAULT_RETRY_OPTIONS): MethodDecorator { + return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value as (...args: any[]) => any + + descriptor.value = async function (...args: any[]) { + const methodName = propertyKey.toString() + return await withRetry(() => originalMethod.apply(this, args), options, methodName) + } + + return descriptor + } +} diff --git a/foundations/core/packages/retry/src/delay.ts b/foundations/core/packages/retry/src/delay.ts new file mode 100644 index 0000000000..5adbd17d36 --- /dev/null +++ b/foundations/core/packages/retry/src/delay.ts @@ -0,0 +1,171 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export const DelayStrategyFactory = { + /** + * Create a fixed delay strategy + */ + fixed (options: FixedDelayOptions): DelayStrategy { + return new FixedDelayStrategy(options) + }, + + /** + * Create an exponential backoff delay strategy + */ + exponentialBackoff (options: ExponentialBackoffOptions): DelayStrategy { + return new ExponentialBackoffStrategy(options) + }, + + /** + * Create a Fibonacci delay strategy + */ + fibonacci (options: FibonacciDelayOptions): DelayStrategy { + return new FibonacciDelayStrategy(options) + } +} + +export interface DelayStrategy { + getDelay: (attempt: number) => number +} + +export interface FixedDelayOptions { + /** Delay between retries in milliseconds */ + delayMs: number + /** Optional jitter factor (0-1) to add randomness to delay times */ + jitter?: number +} + +/** + * Fixed delay strategy - uses the same delay for all retry attempts + */ +export class FixedDelayStrategy implements DelayStrategy { + private readonly delayMs: number + private readonly jitter: number + + constructor (options: FixedDelayOptions) { + this.delayMs = options.delayMs + this.jitter = options.jitter ?? 0 + } + + getDelay (_attempt: number): number { + if (this.jitter > 0) { + const jitterAmount = this.delayMs * this.jitter * (Math.random() * 2 - 1) + return Math.max(0, this.delayMs + jitterAmount) + } + return this.delayMs + } +} + +export interface ExponentialBackoffOptions { + /** Initial delay between retries in milliseconds */ + initialDelayMs: number + /** Maximum delay between retries in milliseconds */ + maxDelayMs: number + /** Backoff factor for exponential delay increase */ + backoffFactor: number + /** Optional jitter factor (0-1) to add randomness to delay times */ + jitter?: number +} + +/** + * Exponential backoff delay strategy - increases delay exponentially with each attempt + */ +export class ExponentialBackoffStrategy implements DelayStrategy { + private readonly initialDelayMs: number + private readonly maxDelayMs: number + private readonly backoffFactor: number + private readonly jitter: number + + constructor (options: ExponentialBackoffOptions) { + this.initialDelayMs = options.initialDelayMs + this.maxDelayMs = options.maxDelayMs + this.backoffFactor = options.backoffFactor + this.jitter = options.jitter ?? 0 + } + + getDelay (attempt: number): number { + const baseDelay = Math.min(this.initialDelayMs * Math.pow(this.backoffFactor, attempt - 1), this.maxDelayMs) + + if (this.jitter > 0) { + const jitterAmount = baseDelay * this.jitter * (Math.random() * 2 - 1) + return Math.min(Math.max(0, baseDelay + jitterAmount), this.maxDelayMs) + } + + return baseDelay + } +} + +export interface FibonacciDelayOptions { + /** Base unit for calculating Fibonacci sequence in milliseconds */ + baseDelayMs: number + /** Maximum delay between retries in milliseconds */ + maxDelayMs: number + /** Optional jitter factor (0-1) to add randomness to delay times */ + jitter?: number +} + +export class FibonacciDelayStrategy implements DelayStrategy { + private readonly baseDelayMs: number + private readonly maxDelayMs: number + private readonly jitter: number + + // Cache for Fibonacci numbers to improve performance + private readonly fibCache: Map + + constructor (options: FibonacciDelayOptions) { + this.baseDelayMs = options.baseDelayMs + this.maxDelayMs = options.maxDelayMs + this.jitter = options.jitter ?? 0 + this.fibCache = new Map([ + [0, 0], + [1, 1] + ]) + } + + private fibonacci (n: number): number { + // Return from cache if available + if (this.fibCache.has(n)) { + return this.fibCache.get(n) as number + } + + if (n <= 1) { + return n + } + + // Calculate using recursion with memoization + const result = this.fibonacci(n - 1) + this.fibonacci(n - 2) + this.fibCache.set(n, result) + return result + } + + getDelay (attempt: number): number { + const fibNumber = this.fibonacci(attempt + 1) + const baseDelay = Math.min(fibNumber * this.baseDelayMs, this.maxDelayMs) + + if (this.jitter > 0) { + const jitterAmount = baseDelay * this.jitter * (Math.random() * 2 - 1) + return Math.min(Math.max(0, baseDelay + jitterAmount), this.maxDelayMs) + } + + return baseDelay + } +} + +/** + * Promise-based sleep function + */ +export function sleep (ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/foundations/core/packages/retry/src/index.ts b/foundations/core/packages/retry/src/index.ts new file mode 100644 index 0000000000..3ce12b411b --- /dev/null +++ b/foundations/core/packages/retry/src/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './retry' +export * from './decorator' +export * from './retryable' diff --git a/foundations/core/packages/retry/src/logger.ts b/foundations/core/packages/retry/src/logger.ts new file mode 100644 index 0000000000..6bf7b99066 --- /dev/null +++ b/foundations/core/packages/retry/src/logger.ts @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +export interface Logger { + warn: (message: string, meta?: Record) => void + error: (message: string, meta?: Record) => void + info: (message: string, meta?: Record) => void +} + +export const defaultLogger: Logger = { + warn: (message: string, meta?: Record) => { + console.warn(`[WARN] ${message}`, meta) + }, + error: (message: string, meta?: Record) => { + console.error(`[ERROR] ${message}`, meta) + }, + info: (message: string, meta?: Record) => { + console.info(`[INFO] ${message}`, meta) + } +} diff --git a/foundations/core/packages/retry/src/retry.ts b/foundations/core/packages/retry/src/retry.ts new file mode 100644 index 0000000000..692b602660 --- /dev/null +++ b/foundations/core/packages/retry/src/retry.ts @@ -0,0 +1,128 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { defaultLogger, type Logger } from './logger' +import { type IsRetryable, retryNetworkErrors } from './retryable' +import { type DelayStrategy, DelayStrategyFactory, sleep } from './delay' + +/** + * Configuration options for the retry mechanism + */ +export interface RetryOptions { + /** Maximum number of retry attempts */ + maxRetries: number + /** Function to determine if an error is retriable */ + isRetryable: IsRetryable + /** Strategy for calculating delay between retries */ + delayStrategy: DelayStrategy + /** Logger to use (defaults to console logger) */ + logger?: Logger +} + +/** + * Default retry options + */ +export const DEFAULT_RETRY_OPTIONS: RetryOptions = { + maxRetries: 5, + isRetryable: retryNetworkErrors, + delayStrategy: DelayStrategyFactory.exponentialBackoff({ + initialDelayMs: 1000, + maxDelayMs: 30000, + backoffFactor: 1.5, + jitter: 0.2 + }), + logger: defaultLogger +} + +/** + * Executes an operation with exponential backoff retry + * + * @param operation - Async operation to execute + * @param options - Retry configuration options + * @param operationName - Name of the operation for logging + * @returns The result of the operation + * @throws The last error encountered after all retries have been exhausted + */ +export async function withRetry ( + operation: () => Promise, + options: Partial = {}, + operationName: string = 'operation' +): Promise { + const config: RetryOptions = { ...DEFAULT_RETRY_OPTIONS, ...options } + const logger = config.logger ?? defaultLogger + let attempt = 1 + let lastError: Error | undefined + + while (attempt <= config.maxRetries) { + try { + return await operation() + } catch (error: any) { + lastError = error + const isLastAttempt = attempt >= config.maxRetries + + if (isLastAttempt) { + logger.error(`${operationName} failed after ${attempt} attempts`, { + error, + attempt, + maxRetries: config.maxRetries + }) + throw error + } + if (!config.isRetryable(error)) { + logger.error(`${operationName} failed with non-retriable error`, { + error, + attempt, + maxRetries: config.maxRetries + }) + throw error + } + + // Get delay for next attempt from strategy + const delayMs = Math.round(config.delayStrategy.getDelay(attempt)) + + logger.warn(`${operationName} failed, retrying in ${delayMs}ms`, { + error, + attempt, + nextAttempt: attempt + 1, + delayMs + }) + + // Wait before retry + await sleep(delayMs) + attempt++ + } + } + + // This should not be reached due to the throw in the last iteration + throw lastError ?? new Error(`${operationName} failed for unknown reason`) +} + +/** + * Creates a retryable function from a base function + * Returns a wrapped function that will apply retry logic automatically + * + * @param fn - The function to make retryable + * @param options - Retry configuration options + * @param operationName - Name of the operation for logging + * @returns A wrapped function that applies retry logic + */ +export function createRetryableFunction Promise> ( + fn: T, + options: Partial = {}, + operationName: string = 'operation' +): T { + return (async (...args: Parameters): Promise> => { + return await (withRetry(() => fn(...args), options, operationName) as Promise>) + }) as T +} diff --git a/foundations/core/packages/retry/src/retryable.ts b/foundations/core/packages/retry/src/retryable.ts new file mode 100644 index 0000000000..d7e5ac2efb --- /dev/null +++ b/foundations/core/packages/retry/src/retryable.ts @@ -0,0 +1,96 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export type IsRetryable = (error: Error | unknown) => boolean + +export const retryAllErrors: IsRetryable = (_error: Error | unknown): boolean => { + return true +} + +const NETWORK_ERROR_NAMES = new Set([ + 'NetworkError', + 'FetchError', + 'AbortError', + 'TimeoutError', + 'ConnectionError', + 'ConnectionRefusedError', + 'ETIMEDOUT', + 'ECONNREFUSED', + 'ECONNRESET', + 'ENOTFOUND', + 'EAI_AGAIN' +]) + +/** + * Patterns in error messages that suggest network issues + */ +const NETWORK_ERROR_PATTERNS = [ + /network/i, + /connection/i, + /timeout/i, + /unreachable/i, + /refused/i, + /reset/i, + /socket/i, + /DNS/i, + /ENOTFOUND/, + /ECONNREFUSED/, + /ECONNRESET/, + /ETIMEDOUT/, + /EAI_AGAIN/ +] + +/** + * Determine if an error is related to network issues + */ +export const retryNetworkErrors: IsRetryable = (error: Error | unknown): boolean => { + if (error == null) { + return false + } + + // Check for error name + if (error instanceof Error) { + // Check if the error name is in our set of network errors + if (NETWORK_ERROR_NAMES.has(error.name)) { + return true + } + + // Check if the error message matches our network error patterns + for (const pattern of NETWORK_ERROR_PATTERNS) { + if (pattern.test(error.message)) { + return true + } + } + + // Check for status codes in response errors + if ('status' in error && typeof (error as any).status === 'number') { + const status = (error as any).status + // Retry server errors (5xx) and some specific client errors + return ( + (status >= 500 && status < 600) || // Server errors + status === 429 || // Too Many Requests + status === 408 || // Request Timeout + status === 423 || // Locked + status === 425 || // Too Early + status === 449 || // Retry With + status === 503 || // Service Unavailable + status === 504 // Gateway Timeout + ) + } + } + + // If we couldn't identify it as a network error, don't retry + return false +} diff --git a/foundations/core/packages/retry/tsconfig.json b/foundations/core/packages/retry/tsconfig.json new file mode 100644 index 0000000000..c6a877cf6c --- /dev/null +++ b/foundations/core/packages/retry/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/rpc/.eslintrc.js b/foundations/core/packages/rpc/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/rpc/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/rpc/.npmignore b/foundations/core/packages/rpc/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/rpc/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/rpc/CHANGELOG.json b/foundations/core/packages/rpc/CHANGELOG.json new file mode 100644 index 0000000000..e5c5509e80 --- /dev/null +++ b/foundations/core/packages/rpc/CHANGELOG.json @@ -0,0 +1,69 @@ +{ + "name": "@hcengineering/rpc", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/rpc_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/rpc_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/rpc_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/rpc_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/rpc/CHANGELOG.md b/foundations/core/packages/rpc/CHANGELOG.md new file mode 100644 index 0000000000..719a9af13e --- /dev/null +++ b/foundations/core/packages/rpc/CHANGELOG.md @@ -0,0 +1,28 @@ +# Change Log - @hcengineering/rpc + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/rpc/config/rig.json b/foundations/core/packages/rpc/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/rpc/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/rpc/jest.config.js b/foundations/core/packages/rpc/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/rpc/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/rpc/package.json b/foundations/core/packages/rpc/package.json new file mode 100644 index 0000000000..972ecc2e59 --- /dev/null +++ b/foundations/core/packages/rpc/package.json @@ -0,0 +1,59 @@ +{ + "name": "@hcengineering/rpc", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent --coverage", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@types/node": "^22.18.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/platform": "workspace:^0.7.18", + "msgpackr": "^1.11.2" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/rpc/src/index.ts b/foundations/core/packages/rpc/src/index.ts new file mode 100644 index 0000000000..17c0812118 --- /dev/null +++ b/foundations/core/packages/rpc/src/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './rpc' +export * from './sliding' diff --git a/foundations/core/packages/rpc/src/rpc.ts b/foundations/core/packages/rpc/src/rpc.ts new file mode 100644 index 0000000000..d1aa70f22d --- /dev/null +++ b/foundations/core/packages/rpc/src/rpc.ts @@ -0,0 +1,216 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Account } from '@hcengineering/core' +import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' +import { Packr } from 'msgpackr' + +/** + * @public + */ +export type ReqId = string | number + +/** + * @public + */ +export interface Request

{ + id?: ReqId + method: string + params: P + + meta?: Record + + time?: number // Server time to perform operation +} + +/** + * @public + */ +export interface HelloRequest extends Request { + binary?: boolean + compression?: boolean +} +/** + * @public + */ +export interface HelloResponse extends Response { + binary: boolean + reconnect?: boolean + serverVersion: string + lastTx?: string + lastHash?: string // Last model hash + account: Account + useCompression?: boolean +} + +function isTotalArray (value: any): value is { total?: number, lookupMap?: Record } & any[] { + return Array.isArray(value) && ((value as any).total !== undefined || (value as any).lookupMap !== undefined) +} +export function rpcJSONReplacer (key: string, value: any): any { + if (isTotalArray(value)) { + return { + dataType: 'TotalArray', + total: value.total, + lookupMap: value.lookupMap, + value: [...value] + } + } else if ( + typeof value === 'object' && + value !== null && + 'domain' in value && + typeof value.domain === 'string' && + 'value' in value && + isTotalArray(value.value) + ) { + return { + ...value, + value: { + dataType: 'TotalArray', + total: value.value.total, + lookupMap: value.value.lookupMap, + value: [...value.value] + } + } + } else { + return value ?? null + } +} + +export function rpcJSONReceiver (key: string, value: any): any { + if (typeof value === 'object' && value !== null) { + if (value.dataType === 'TotalArray') { + return Object.assign(value.value, { total: value.total, lookupMap: value.lookupMap }) + } else if ( + 'domain' in value && + typeof value.domain === 'string' && + 'value' in value && + value.value != null && + value.value.dataType === 'TotalArray' + ) { + return { + ...value, + value: Object.assign(value.value.value, { total: value.value.total, lookupMap: value.value.lookupMap }) + } + } + } + return value +} + +export interface RateLimitInfo { + remaining: number + limit: number + + current: number // in milliseconds + reset: number // in milliseconds + retryAfter?: number // in milliseconds +} + +/** + * Response object define a server response on transaction request. + * Also used to inform other clients about operations being performed by server. + * + * @public + */ +export interface Response { + result?: R + id?: ReqId + error?: Status + terminate?: boolean + + rateLimit?: RateLimitInfo + chunk?: { + index: number + final: boolean + } + time?: number // Server time to perform operation + bfst?: number // Server time to perform operation + queue?: number +} + +export class RPCHandler { + packr = new Packr({ structuredClone: true, bundleStrings: true, copyBuffers: false }) + protoSerialize (object: object, binary: boolean): any { + if (!binary) { + return JSON.stringify(object, rpcJSONReplacer) + } + return new Uint8Array(this.packr.pack(object)) + } + + protoDeserialize (data: any, binary: boolean): any { + if (!binary) { + let _data = data + if (_data instanceof ArrayBuffer) { + const decoder = new TextDecoder() + _data = decoder.decode(_data) + } + try { + return JSON.parse(_data.toString(), rpcJSONReceiver) + } catch (err: any) { + if (((err.message as string) ?? '').includes('Unexpected token')) { + return this.packr.unpack(new Uint8Array(data)) + } + } + } + return this.packr.unpack(new Uint8Array(data)) + } + + /** + * @public + * @param object - + * @returns + */ + serialize (object: Request | Response, binary: boolean): any { + if ((object as any).result !== undefined) { + ;(object as any).result = rpcJSONReplacer('result', (object as any).result) + } + return this.protoSerialize(object, binary) + } + + /** + * @public + * @param response - + * @returns + */ + readResponse(response: any, binary: boolean): Response { + const data = this.protoDeserialize(response, binary) + if (data.result !== undefined) { + data.result = rpcJSONReceiver('result', data.result) + } + return data + } + + /** + * @public + * @param request - + * @returns + */ + readRequest

(request: any, binary: boolean): Request

{ + const result: Request

= this.protoDeserialize(request, binary) + if (typeof result.method !== 'string') { + throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})) + } + return result + } +} + +/** + * @public + * @param status - + * @param id - + * @returns + */ +export function fromStatus (status: Status, id?: ReqId): Response { + return { id, error: status } +} diff --git a/foundations/core/packages/rpc/src/sliding.ts b/foundations/core/packages/rpc/src/sliding.ts new file mode 100644 index 0000000000..15ffbfa275 --- /dev/null +++ b/foundations/core/packages/rpc/src/sliding.ts @@ -0,0 +1,74 @@ +import type { RateLimitInfo } from './rpc' + +export class SlidingWindowRateLimitter { + private readonly rateLimits = new Map< + string, + { + requests: number[] + rejectedRequests: number // Counter for rejected requests + resetTime: number + } + >() + + constructor ( + readonly rateLimitMax: number, + readonly rateLimitWindow: number, + readonly now: () => number = Date.now + ) { + this.rateLimitMax = rateLimitMax + this.rateLimitWindow = rateLimitWindow + } + + public checkRateLimit (groupId: string): RateLimitInfo { + const now = this.now() + const windowStart = now - this.rateLimitWindow + + let rateLimit = this.rateLimits.get(groupId) + if (rateLimit == null) { + rateLimit = { requests: [], resetTime: now + this.rateLimitWindow, rejectedRequests: 0 } + this.rateLimits.set(groupId, rateLimit) + } + + // Remove requests outside the current window + rateLimit.requests = rateLimit.requests.filter((time) => time > windowStart) + + // Reset rejected requests counter when window changes + if (rateLimit.requests.length === 0) { + rateLimit.rejectedRequests = 0 + } + + // Update reset time + rateLimit.resetTime = now + this.rateLimitWindow + + if (rateLimit.requests.length <= this.rateLimitMax) { + rateLimit.requests.push(now + (rateLimit.rejectedRequests > this.rateLimitMax * 2 ? this.rateLimitWindow * 5 : 0)) + } + + if (rateLimit.requests.length >= this.rateLimitMax) { + rateLimit.rejectedRequests++ + + // Find when the oldest request will exit the window + const nextAvailableTime = rateLimit.requests[0] + this.rateLimitWindow + + return { + remaining: 0, + limit: this.rateLimitMax, + current: rateLimit.requests.length, + reset: rateLimit.resetTime, + retryAfter: Math.max(1, nextAvailableTime - now + 1) + } + } + + return { + remaining: this.rateLimitMax - rateLimit.requests.length, + current: rateLimit.requests.length, + limit: this.rateLimitMax, + reset: rateLimit.resetTime + } + } + + // Add a reset method for testing purposes + public reset (): void { + this.rateLimits.clear() + } +} diff --git a/foundations/core/packages/rpc/src/test/rateLimit.spec.ts b/foundations/core/packages/rpc/src/test/rateLimit.spec.ts new file mode 100644 index 0000000000..cdafa573db --- /dev/null +++ b/foundations/core/packages/rpc/src/test/rateLimit.spec.ts @@ -0,0 +1,128 @@ +import { SlidingWindowRateLimitter } from '../sliding' + +describe('SlidingWindowRateLimitter', () => { + let clock = 100000 + + beforeEach(() => { + // Mock Date.now to control time + clock = 100000 + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('should allow requests within the limit', () => { + const limiter = new SlidingWindowRateLimitter(5, 60000, () => clock) + + for (let i = 0; i < 5; i++) { + const result = limiter.checkRateLimit('user1') + expect(result.remaining).toBe(5 - i - 1) + expect(result.limit).toBe(5) + } + + // The next request should hit the limit + const result = limiter.checkRateLimit('user1') + expect(result.remaining).toBe(0) + expect(result.retryAfter).toBeDefined() + }) + + it('should reject requests beyond the limit', () => { + const limiter = new SlidingWindowRateLimitter(3, 60000, () => clock) + + // Use up the limit + limiter.checkRateLimit('user1') + limiter.checkRateLimit('user1') + limiter.checkRateLimit('user1') + + // This should be limited + const result = limiter.checkRateLimit('user1') + expect(result.remaining).toBe(0) + expect(result.retryAfter).toBeDefined() + }) + + it('should allow new requests as the window slides', () => { + const limiter = new SlidingWindowRateLimitter(2, 10000, () => clock) + + // Use up the limit + limiter.checkRateLimit('user1') + limiter.checkRateLimit('user1') + + // This should be limited + expect(limiter.checkRateLimit('user1').remaining).toBe(0) + + // Move time forward by 5 seconds (half the window) + clock += 5 * 1000 // 5 seconds + + // Should still have one request outside the current window + // and one within, so we can make one more request + const result = limiter.checkRateLimit('user1') + expect(result.remaining).toBe(0) // Now at limit again + + // Move time forward by full window + clock += 11 * 1000 // 1011 seconds + + // All previous requests should be outside the window + const newResult = limiter.checkRateLimit('user1') + expect(newResult.remaining).toBe(1) // One request used, one remaining + expect(limiter.checkRateLimit('user1').remaining).toBe(0) // Now at limit again + }) + + it('should handle different identifiers separately', () => { + const limiter = new SlidingWindowRateLimitter(2, 60000, () => clock) + + limiter.checkRateLimit('user1') + limiter.checkRateLimit('user1') + + // User1 should be at limit + expect(limiter.checkRateLimit('user1').remaining).toBe(0) + + // Different user should have separate limit + expect(limiter.checkRateLimit('user2').remaining).toBe(1) + expect(limiter.checkRateLimit('user2').remaining).toBe(0) + + // Both users should be at their limits + expect(limiter.checkRateLimit('user1').remaining).toBe(0) + expect(limiter.checkRateLimit('user2').remaining).toBe(0) + }) + + it('should handle sliding window correctly', () => { + const limiter = new SlidingWindowRateLimitter(10, 60000, () => clock) + + // Use up half the capacity + for (let i = 0; i < 5; i++) { + limiter.checkRateLimit('user1') + } + + // Move halfway through the window + clock += 30 * 1000 + 1 // 30 seconds + + // Make some more requests + for (let i = 0; i < 7; i++) { + const result = limiter.checkRateLimit('user1') + if (i < 5) { + expect(result.remaining).toBeGreaterThanOrEqual(0) + } else { + expect(result.remaining).toBe(0) + expect(result.retryAfter).toBeDefined() + break + } + } + }) + + it('check for ban', () => { + const limiter = new SlidingWindowRateLimitter(10, 10000, () => clock) + + for (let i = 0; i < 50; i++) { + limiter.checkRateLimit('user1') + } + + const r1 = limiter.checkRateLimit('user1') + expect(r1.remaining).toBe(0) + // Pass all window time. + clock += 10000 + + const r2 = limiter.checkRateLimit('user1') + expect(r2.remaining).toBe(9) + }) +}) diff --git a/foundations/core/packages/rpc/tsconfig.json b/foundations/core/packages/rpc/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/rpc/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/storage-client/.eslintrc.js b/foundations/core/packages/storage-client/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/storage-client/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/storage-client/.npmignore b/foundations/core/packages/storage-client/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/storage-client/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/storage-client/CHANGELOG.json b/foundations/core/packages/storage-client/CHANGELOG.json new file mode 100644 index 0000000000..afb0fb0f70 --- /dev/null +++ b/foundations/core/packages/storage-client/CHANGELOG.json @@ -0,0 +1,73 @@ +{ + "name": "@hcengineering/storage-client", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/storage-client_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.8", + "tag": "@hcengineering/storage-client_v0.7.8", + "date": "Thu, 23 Oct 2025 18:03:30 GMT", + "comments": { + "patch": [ + { + "comment": "use proper content type in multipart upload" + } + ] + } + }, + { + "version": "0.7.7", + "tag": "@hcengineering/storage-client_v0.7.7", + "date": "Tue, 21 Oct 2025 19:04:55 GMT", + "comments": { + "patch": [ + { + "comment": "more accurate upload progress" + }, + { + "comment": "update integration tests" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.8` to `0.7.10`" + } + ] + } + }, + { + "version": "0.7.6", + "tag": "@hcengineering/storage-client_v0.7.6", + "date": "Tue, 14 Oct 2025 15:36:13 GMT", + "comments": { + "patch": [ + { + "comment": "fix front service upload" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/storage-client_v0.7.5", + "date": "Tue, 14 Oct 2025 06:29:02 GMT", + "comments": { + "patch": [ + { + "comment": "new storage client" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/storage-client/CHANGELOG.md b/foundations/core/packages/storage-client/CHANGELOG.md new file mode 100644 index 0000000000..0244089935 --- /dev/null +++ b/foundations/core/packages/storage-client/CHANGELOG.md @@ -0,0 +1,38 @@ +# Change Log - @hcengineering/storage-client + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.8 +Thu, 23 Oct 2025 18:03:30 GMT + +### Patches + +- use proper content type in multipart upload + +## 0.7.7 +Tue, 21 Oct 2025 19:04:55 GMT + +### Patches + +- more accurate upload progress +- update integration tests + +## 0.7.6 +Tue, 14 Oct 2025 15:36:13 GMT + +### Patches + +- fix front service upload + +## 0.7.5 +Tue, 14 Oct 2025 06:29:02 GMT + +### Patches + +- new storage client + diff --git a/foundations/core/packages/storage-client/config/rig.json b/foundations/core/packages/storage-client/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/storage-client/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/storage-client/jest.config.js b/foundations/core/packages/storage-client/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/storage-client/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/storage-client/package.json b/foundations/core/packages/storage-client/package.json new file mode 100644 index 0000000000..46968f9f8e --- /dev/null +++ b/foundations/core/packages/storage-client/package.json @@ -0,0 +1,58 @@ +{ + "name": "@hcengineering/storage-client", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent --coverage", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "cross-env": "~7.0.3", + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@types/node": "^22.18.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "esbuild": "^0.25.10", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/storage-client/src/__tests__/create-file-storage.test.ts b/foundations/core/packages/storage-client/src/__tests__/create-file-storage.test.ts new file mode 100644 index 0000000000..933f513364 --- /dev/null +++ b/foundations/core/packages/storage-client/src/__tests__/create-file-storage.test.ts @@ -0,0 +1,343 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { createFileStorage, FileStorageConfig } from '../client' +import { DatalakeStorage } from '../client/datalake' +import { FrontStorage } from '../client/front' +import { HulylakeStorage } from '../client/hulylake' + +describe('createFileStorage factory', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('DatalakeStorage creation', () => { + it('should create DatalakeStorage when datalakeUrl is provided', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: 'https://datalake.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(DatalakeStorage) + }) + + it('should create DatalakeStorage when both datalakeUrl and hulylakeUrl are provided (datalake takes precedence)', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: 'https://datalake.example.com', + hulylakeUrl: 'https://hulylake.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(DatalakeStorage) + }) + + it('should create DatalakeStorage when datalakeUrl is non-empty string', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: 'https://datalake.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(DatalakeStorage) + }) + + it('should not create DatalakeStorage when datalakeUrl is empty string', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: '' + } + + const storage = createFileStorage(config) + + expect(storage).not.toBeInstanceOf(DatalakeStorage) + expect(storage).toBeInstanceOf(FrontStorage) + }) + + it('should not create DatalakeStorage when datalakeUrl is undefined', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: undefined + } + + const storage = createFileStorage(config) + + expect(storage).not.toBeInstanceOf(DatalakeStorage) + expect(storage).toBeInstanceOf(FrontStorage) + }) + }) + + describe('HulylakeStorage creation', () => { + it('should create HulylakeStorage when hulylakeUrl is provided and datalakeUrl is not', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + hulylakeUrl: 'https://hulylake.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(HulylakeStorage) + }) + + it('should create HulylakeStorage when hulylakeUrl is provided and datalakeUrl is empty', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: '', + hulylakeUrl: 'https://hulylake.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(HulylakeStorage) + }) + + it('should create HulylakeStorage when hulylakeUrl is provided and datalakeUrl is undefined', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: undefined, + hulylakeUrl: 'https://hulylake.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(HulylakeStorage) + }) + + it('should not create HulylakeStorage when hulylakeUrl is empty string', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + hulylakeUrl: '' + } + + const storage = createFileStorage(config) + + expect(storage).not.toBeInstanceOf(HulylakeStorage) + expect(storage).toBeInstanceOf(FrontStorage) + }) + + it('should not create HulylakeStorage when hulylakeUrl is undefined', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + hulylakeUrl: undefined + } + + const storage = createFileStorage(config) + + expect(storage).not.toBeInstanceOf(HulylakeStorage) + expect(storage).toBeInstanceOf(FrontStorage) + }) + }) + + describe('FrontStorage creation', () => { + it('should create FrontStorage when only uploadUrl is provided', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(FrontStorage) + }) + + it('should create FrontStorage when all URLs are empty strings', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: '', + hulylakeUrl: '' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(FrontStorage) + }) + + it('should create FrontStorage when all optional URLs are undefined', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: undefined, + hulylakeUrl: undefined + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(FrontStorage) + }) + + it('should create FrontStorage as fallback', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(FrontStorage) + }) + }) + + describe('priority and precedence', () => { + it('should prioritize DatalakeStorage over HulylakeStorage', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: 'https://datalake.example.com', + hulylakeUrl: 'https://hulylake.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(DatalakeStorage) + expect(storage).not.toBeInstanceOf(HulylakeStorage) + }) + + it('should prioritize HulylakeStorage over FrontStorage', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + hulylakeUrl: 'https://hulylake.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(HulylakeStorage) + expect(storage).not.toBeInstanceOf(FrontStorage) + }) + + it('should use FrontStorage when no specialized storage is available', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(FrontStorage) + expect(storage).not.toBeInstanceOf(DatalakeStorage) + expect(storage).not.toBeInstanceOf(HulylakeStorage) + }) + }) + + describe('URL handling', () => { + it('should pass correct URL to DatalakeStorage', () => { + const datalakeUrl = 'https://datalake.example.com' + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl + } + + const storage = createFileStorage(config) as DatalakeStorage + + // Test that the URL was passed correctly by checking the generated file URL + const fileUrl = storage.getFileUrl('test-workspace', 'test-file', 'test.txt') + expect(fileUrl).toContain(datalakeUrl) + }) + + it('should pass correct URL to HulylakeStorage', () => { + const hulylakeUrl = 'https://hulylake.example.com' + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + hulylakeUrl + } + + const storage = createFileStorage(config) as HulylakeStorage + + // Test that the URL was passed correctly by checking the generated file URL + const fileUrl = storage.getFileUrl('test-workspace', 'test-file', 'test.txt') + expect(fileUrl).toContain(hulylakeUrl) + }) + + it('should pass correct URL to FrontStorage', () => { + const uploadUrl = 'https://upload.example.com' + const config: FileStorageConfig = { + uploadUrl + } + + const storage = createFileStorage(config) as FrontStorage + + // Test that the URL was passed correctly by checking the generated file URL + const fileUrl = storage.getFileUrl('test-workspace', 'test-file', 'test.txt') + expect(fileUrl).toContain(uploadUrl) + }) + }) + + describe('edge cases', () => { + it('should handle config with extra properties', () => { + const config: FileStorageConfig & { extraProp: string } = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: 'https://datalake.example.com', + extraProp: 'ignored' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(DatalakeStorage) + }) + + it('should handle URLs with trailing slashes', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com/', + datalakeUrl: 'https://datalake.example.com/' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(DatalakeStorage) + }) + + it('should handle URLs without trailing slashes', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + hulylakeUrl: 'https://hulylake.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(HulylakeStorage) + }) + + it('should handle URLs with different protocols', () => { + const config: FileStorageConfig = { + uploadUrl: 'http://upload.example.com', + datalakeUrl: 'http://datalake.example.com' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(DatalakeStorage) + }) + + it('should handle URLs with ports', () => { + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com:8080', + hulylakeUrl: 'https://hulylake.example.com:9090' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(HulylakeStorage) + }) + + it('should handle localhost URLs', () => { + const config: FileStorageConfig = { + uploadUrl: 'http://localhost:3000', + datalakeUrl: 'http://localhost:4000' + } + + const storage = createFileStorage(config) + + expect(storage).toBeInstanceOf(DatalakeStorage) + }) + }) +}) diff --git a/foundations/core/packages/storage-client/src/__tests__/datalake-storage.test.ts b/foundations/core/packages/storage-client/src/__tests__/datalake-storage.test.ts new file mode 100644 index 0000000000..fcdfa9099c --- /dev/null +++ b/foundations/core/packages/storage-client/src/__tests__/datalake-storage.test.ts @@ -0,0 +1,464 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { DatalakeStorage } from '../client/datalake' +import * as upload from '../upload' + +// Mock the upload module +jest.mock('../upload') +const mockUploadXhr = jest.mocked(upload.uploadXhr) +const mockUploadMultipart = jest.mocked(upload.uploadMultipart) + +// Mock fetch +const mockFetch = jest.fn() +global.fetch = mockFetch as any + +describe('DatalakeStorage', () => { + let storage: DatalakeStorage + const baseUrl = 'https://datalake.example.com' + + beforeEach(() => { + storage = new DatalakeStorage(baseUrl) + jest.clearAllMocks() + mockFetch.mockClear() + mockUploadXhr.mockClear() + mockUploadMultipart.mockClear() + }) + + describe('getFileUrl', () => { + it('should generate correct URL with filename', () => { + const workspace = 'test-workspace' + const file = 'file-123' + const filename = 'document.pdf' + + const url = storage.getFileUrl(workspace, file, filename) + + expect(url).toBe(`${baseUrl}/blob/${workspace}/${file}/${filename}`) + }) + + it('should generate correct URL without filename', () => { + const workspace = 'test-workspace' + const file = 'file-123' + + const url = storage.getFileUrl(workspace, file) + + expect(url).toBe(`${baseUrl}/blob/${workspace}/${file}`) + }) + + it('should handle special characters in parameters', () => { + const workspace = 'test workspace' + const file = 'file 123' + const filename = 'my document.pdf' + + const url = storage.getFileUrl(workspace, file, filename) + + expect(url).toBe(`${baseUrl}/blob/${workspace}/${file}/${filename}`) + }) + + it('should handle base URL with trailing slash', () => { + const storageWithSlash = new DatalakeStorage('https://datalake.example.com/') + const workspace = 'test-workspace' + const file = 'file-123' + const filename = 'test.txt' + + const url = storageWithSlash.getFileUrl(workspace, file, filename) + + expect(url).toBe('https://datalake.example.com/blob/test-workspace/file-123/test.txt') + }) + }) + + describe('getFileMeta', () => { + it('should fetch file metadata successfully', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + const expectedMeta = { + size: 1024, + contentType: 'text/plain', + lastModified: '2023-01-01T00:00:00Z' + } + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(expectedMeta) + }) + + const meta = await storage.getFileMeta(token, workspace, file) + + expect(meta).toEqual(expectedMeta) + expect(mockFetch).toHaveBeenCalledWith( + `${baseUrl}/meta/${encodeURIComponent(workspace)}/${encodeURIComponent(file)}`, + { + headers: { + Authorization: `Bearer ${token}` + } + } + ) + }) + + it('should return empty object when metadata fetch fails', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404 + }) + + const meta = await storage.getFileMeta(token, workspace, file) + + expect(meta).toEqual({}) + }) + + it('should return empty object when network error occurs', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + mockFetch.mockRejectedValueOnce(new Error('Network error')) + + const meta = await storage.getFileMeta(token, workspace, file) + + expect(meta).toEqual({}) + }) + + it('should handle special characters in workspace and file names', async () => { + const token = 'test-token' + const workspace = 'test workspace' + const file = 'file 123' + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({}) + }) + + await storage.getFileMeta(token, workspace, file) + + expect(mockFetch).toHaveBeenCalledWith( + `${baseUrl}/meta/${encodeURIComponent(workspace)}/${encodeURIComponent(file)}`, + { + headers: { + Authorization: `Bearer ${token}` + } + } + ) + }) + }) + + describe('deleteFile', () => { + it('should delete file successfully', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200 + }) + + await storage.deleteFile(token, workspace, file) + + expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/blob/${workspace}/${file}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}` + } + }) + }) + + it('should throw error when delete fails', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found' + }) + + await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Failed to delete file') + }) + + it('should handle network errors during delete', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + mockFetch.mockRejectedValueOnce(new Error('Network error')) + + await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Network error') + }) + }) + + describe('uploadFile', () => { + describe('small file upload (≤ 10MB)', () => { + it('should upload small file using form-data', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 5 * 1024 * 1024 }) // 5MB + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadXhr).toHaveBeenCalledWith( + { + url: `${baseUrl}/upload/form-data/${encodeURIComponent(workspace)}`, + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + body: expect.any(FormData) + }, + undefined + ) + + // Check FormData contents + const uploadCall = mockUploadXhr.mock.calls[0] + const formData = uploadCall[0].body as FormData + const uploadedFile = formData.get('file') as File + expect(uploadedFile).toBeInstanceOf(File) + expect(uploadedFile.name).toBe(uuid) // DatalakeStorage renames the file to uuid + + expect(mockUploadMultipart).not.toHaveBeenCalled() + }) + + it('should upload exactly 10MB file using form-data', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 10 * 1024 * 1024 }) // exactly 10MB + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadXhr).toHaveBeenCalled() + expect(mockUploadMultipart).not.toHaveBeenCalled() + }) + + it('should pass upload options to uploadXhr', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 1024 }) // 1KB + const onProgress = jest.fn() + const controller = new AbortController() + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file, { + onProgress, + signal: controller.signal + }) + + expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), { onProgress, signal: controller.signal }) + }) + }) + + describe('large file upload (> 10MB)', () => { + it('should upload large file using multipart', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'large-file-uuid' + const file = new File(['large content'], 'large.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }) // 15MB + + mockUploadMultipart.mockResolvedValueOnce() + + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadMultipart).toHaveBeenCalledWith( + { + url: `${baseUrl}/upload/multipart/${encodeURIComponent(workspace)}/${encodeURIComponent(uuid)}`, + headers: { Authorization: `Bearer ${token}` }, + body: file + }, + undefined + ) + + expect(mockUploadXhr).not.toHaveBeenCalled() + }) + + it('should pass upload options to uploadMultipart', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'large-file-uuid' + const file = new File(['large content'], 'large.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 20 * 1024 * 1024 }) // 20MB + const onProgress = jest.fn() + const controller = new AbortController() + + mockUploadMultipart.mockResolvedValueOnce() + + await storage.uploadFile(token, workspace, uuid, file, { + onProgress, + signal: controller.signal + }) + + expect(mockUploadMultipart).toHaveBeenCalledWith(expect.any(Object), { onProgress, signal: controller.signal }) + }) + + it('should handle special characters in workspace and uuid for multipart', async () => { + const token = 'test-token' + const workspace = 'test workspace' + const uuid = 'file uuid 123' + const file = new File(['large content'], 'large.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }) // 15MB + + mockUploadMultipart.mockResolvedValueOnce() + + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadMultipart).toHaveBeenCalledWith( + { + url: `${baseUrl}/upload/multipart/${encodeURIComponent(workspace)}/${encodeURIComponent(uuid)}`, + headers: { Authorization: `Bearer ${token}` }, + body: file + }, + undefined + ) + }) + }) + + describe('error handling', () => { + it('should handle small file upload errors', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 1024 }) // 1KB + + mockUploadXhr.mockRejectedValueOnce(new Error('Upload failed')) + + await expect(storage.uploadFile(token, workspace, uuid, file)).rejects.toThrow('Upload failed') + }) + + it('should handle large file upload errors', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'large-file-uuid' + const file = new File(['large content'], 'large.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }) // 15MB + + mockUploadMultipart.mockRejectedValueOnce(new Error('Multipart upload failed')) + + await expect(storage.uploadFile(token, workspace, uuid, file)).rejects.toThrow('Multipart upload failed') + }) + }) + }) + + describe('integration scenarios', () => { + it('should handle complete file lifecycle with small file', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'lifecycle-uuid' + const filename = 'lifecycle.txt' + const file = new File(['test content'], filename, { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 1024 }) // 1KB + + // Upload small file + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + await storage.uploadFile(token, workspace, uuid, file) + + // Get file URL + const url = storage.getFileUrl(workspace, uuid, filename) + expect(url).toBe(`${baseUrl}/blob/${workspace}/${uuid}/${filename}`) + + // Get file meta + const expectedMeta = { size: 1024, contentType: 'text/plain' } + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(expectedMeta) + }) + const meta = await storage.getFileMeta(token, workspace, uuid) + expect(meta).toEqual(expectedMeta) + + // Delete file + mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) + await storage.deleteFile(token, workspace, uuid) + + expect(mockUploadXhr).toHaveBeenCalledTimes(1) + expect(mockFetch).toHaveBeenCalledTimes(2) // meta + delete + }) + + it('should handle complete file lifecycle with large file', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'large-lifecycle-uuid' + const filename = 'large-lifecycle.txt' + const file = new File(['large content'], filename, { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }) // 15MB + + // Upload large file + mockUploadMultipart.mockResolvedValueOnce() + await storage.uploadFile(token, workspace, uuid, file) + + // Get file URL + const url = storage.getFileUrl(workspace, uuid, filename) + expect(url).toBe(`${baseUrl}/blob/${workspace}/${uuid}/${filename}`) + + // Get file meta + const expectedMeta = { size: 15 * 1024 * 1024, contentType: 'text/plain' } + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(expectedMeta) + }) + const meta = await storage.getFileMeta(token, workspace, uuid) + expect(meta).toEqual(expectedMeta) + + // Delete file + mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) + await storage.deleteFile(token, workspace, uuid) + + expect(mockUploadMultipart).toHaveBeenCalledTimes(1) + expect(mockUploadXhr).not.toHaveBeenCalled() + expect(mockFetch).toHaveBeenCalledTimes(2) // meta + delete + }) + + it('should handle upload size threshold boundary', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + + // Test file exactly at 10MB (should use form-data) + const smallUuid = 'small-boundary-uuid' + const smallFile = new File(['content'], 'small.txt', { type: 'text/plain' }) + Object.defineProperty(smallFile, 'size', { value: 10 * 1024 * 1024 }) + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + await storage.uploadFile(token, workspace, smallUuid, smallFile) + expect(mockUploadXhr).toHaveBeenCalled() + expect(mockUploadMultipart).not.toHaveBeenCalled() + + // Reset mocks + mockUploadXhr.mockClear() + mockUploadMultipart.mockClear() + + // Test file just over 10MB (should use multipart) + const largeUuid = 'large-boundary-uuid' + const largeFile = new File(['content'], 'large.txt', { type: 'text/plain' }) + Object.defineProperty(largeFile, 'size', { value: 10 * 1024 * 1024 + 1 }) + + mockUploadMultipart.mockResolvedValueOnce() + await storage.uploadFile(token, workspace, largeUuid, largeFile) + expect(mockUploadMultipart).toHaveBeenCalled() + expect(mockUploadXhr).not.toHaveBeenCalled() + }) + }) +}) diff --git a/foundations/core/packages/storage-client/src/__tests__/front-storage.test.ts b/foundations/core/packages/storage-client/src/__tests__/front-storage.test.ts new file mode 100644 index 0000000000..5c8e80c6ef --- /dev/null +++ b/foundations/core/packages/storage-client/src/__tests__/front-storage.test.ts @@ -0,0 +1,312 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { FrontStorage } from '../client/front' +import * as upload from '../upload' + +// Mock the upload module +jest.mock('../upload') +const mockUploadXhr = jest.mocked(upload.uploadXhr) + +// Mock fetch +const mockFetch = jest.fn() +global.fetch = mockFetch as any + +describe('FrontStorage', () => { + let storage: FrontStorage + const baseUrl = 'https://files.example.com' + + beforeEach(() => { + storage = new FrontStorage(baseUrl) + jest.clearAllMocks() + mockFetch.mockClear() + mockUploadXhr.mockClear() + }) + + describe('getFileUrl', () => { + it('should generate correct URL with file ID only', () => { + const workspace = 'test-workspace' + const file = 'file-123' + + const url = storage.getFileUrl(workspace, file) + + expect(url).toBe(`${baseUrl}/${workspace}/${file}?file=${file}&workspace=${workspace}`) + }) + + it('should generate correct URL with custom filename', () => { + const workspace = 'test-workspace' + const file = 'file-123' + const filename = 'document.pdf' + + const url = storage.getFileUrl(workspace, file, filename) + + expect(url).toBe(`${baseUrl}/${workspace}/${filename}?file=${file}&workspace=${workspace}`) + }) + + it('should handle special characters in workspace and file names', () => { + const workspace = 'test workspace' + const file = 'file 123' + const filename = 'my document.pdf' + + const url = storage.getFileUrl(workspace, file, filename) + + expect(url).toBe(`${baseUrl}/${workspace}/${filename}?file=${file}&workspace=${workspace}`) + }) + + it('should handle base URL with trailing slash', () => { + const storageWithSlash = new FrontStorage('https://files.example.com/') + const workspace = 'test-workspace' + const file = 'file-123' + + const url = storageWithSlash.getFileUrl(workspace, file) + + expect(url).toBe('https://files.example.com/test-workspace/file-123?file=file-123&workspace=test-workspace') + }) + + it('should handle base URL without trailing slash', () => { + const storageWithoutSlash = new FrontStorage('https://files.example.com') + const workspace = 'test-workspace' + const file = 'file-123' + + const url = storageWithoutSlash.getFileUrl(workspace, file) + + expect(url).toBe('https://files.example.com/test-workspace/file-123?file=file-123&workspace=test-workspace') + }) + }) + + describe('getFileMeta', () => { + it('should return empty object', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + const meta = await storage.getFileMeta(token, workspace, file) + + expect(meta).toEqual({}) + }) + }) + + describe('deleteFile', () => { + it('should delete file successfully', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200 + }) + + await storage.deleteFile(token, workspace, file) + + expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/${workspace}/${file}?file=${file}&workspace=${workspace}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}` + } + }) + }) + + it('should throw error when delete fails', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found' + }) + + await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Failed to delete file') + + expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/${workspace}/${file}?file=${file}&workspace=${workspace}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}` + } + }) + }) + + it('should handle network errors during delete', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + mockFetch.mockRejectedValueOnce(new Error('Network error')) + + await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Network error') + }) + }) + + describe('uploadFile', () => { + it('should upload file successfully', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadXhr).toHaveBeenCalledWith( + { + url: `${baseUrl}/${workspace}`, + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + body: expect.any(FormData) + }, + undefined + ) + + // Check FormData contents + const uploadCall = mockUploadXhr.mock.calls[0] + const formData = uploadCall[0].body as FormData + const uploadedFile = formData.get('file') as File + expect(uploadedFile).toBeInstanceOf(File) + expect(uploadedFile.name).toBe(uuid) + expect(uploadedFile.type).toBe('text/plain') + }) + + it('should upload file with progress tracking', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + const onProgress = jest.fn() + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file, { onProgress }) + + expect(mockUploadXhr).toHaveBeenCalledWith( + { + url: `${baseUrl}/${workspace}`, + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + body: expect.any(FormData) + }, + { onProgress } + ) + }) + + it('should upload file with abort signal', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + const controller = new AbortController() + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file, { signal: controller.signal }) + + expect(mockUploadXhr).toHaveBeenCalledWith( + { + url: `${baseUrl}/${workspace}`, + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + body: expect.any(FormData) + }, + { signal: controller.signal } + ) + }) + + it('should handle upload errors', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + + mockUploadXhr.mockRejectedValueOnce(new Error('Upload failed')) + + await expect(storage.uploadFile(token, workspace, uuid, file)).rejects.toThrow('Upload failed') + }) + + it('should handle different file types', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'image-uuid-123' + const file = new File(['binary data'], 'image.png', { type: 'image/png' }) + + mockUploadXhr.mockResolvedValueOnce({ status: 201, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadXhr).toHaveBeenCalledWith( + expect.objectContaining({ + url: `${baseUrl}/${workspace}`, + method: 'POST', + headers: { Authorization: `Bearer ${token}` } + }), + undefined + ) + + const formData = (mockUploadXhr.mock.calls[0][0] as any).body as FormData + const uploadedFile = formData.get('file') as File + expect(uploadedFile).toBeInstanceOf(File) + expect(uploadedFile.name).toBe(uuid) + expect(uploadedFile.type).toBe('image/png') + }) + + it('should handle empty files', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'empty-uuid-123' + const file = new File([''], 'empty.txt', { type: 'text/plain' }) + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadXhr).toHaveBeenCalled() + const formData = (mockUploadXhr.mock.calls[0][0] as any).body as FormData + const uploadedFile = formData.get('file') as File + expect(uploadedFile).toBeInstanceOf(File) + expect(uploadedFile.name).toBe(uuid) + expect(uploadedFile.type).toBe('text/plain') + }) + }) + + describe('integration scenarios', () => { + it('should handle complete file lifecycle', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'lifecycle-uuid' + const file = new File(['test content'], 'lifecycle.txt', { type: 'text/plain' }) + + // Upload file + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + await storage.uploadFile(token, workspace, uuid, file) + + // Get file URL + const url = storage.getFileUrl(workspace, uuid, 'lifecycle.txt') + expect(url).toContain(uuid) + expect(url).toContain('lifecycle.txt') + + // Get file meta (should return empty object for FrontStorage) + const meta = await storage.getFileMeta(token, workspace, uuid) + expect(meta).toEqual({}) + + // Delete file + mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) + await storage.deleteFile(token, workspace, uuid) + + expect(mockUploadXhr).toHaveBeenCalledTimes(1) + expect(mockFetch).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/foundations/core/packages/storage-client/src/__tests__/hulylake-storage.test.ts b/foundations/core/packages/storage-client/src/__tests__/hulylake-storage.test.ts new file mode 100644 index 0000000000..6f7e6d4e83 --- /dev/null +++ b/foundations/core/packages/storage-client/src/__tests__/hulylake-storage.test.ts @@ -0,0 +1,283 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { HulylakeStorage } from '../client/hulylake' +import * as upload from '../upload' + +// Mock the upload module +jest.mock('../upload') +const mockUploadXhr = jest.mocked(upload.uploadXhr) + +// Mock fetch +const mockFetch = jest.fn() +global.fetch = mockFetch as any + +describe('HulylakeStorage', () => { + let storage: HulylakeStorage + const baseUrl = 'https://hulylake.example.com' + + beforeEach(() => { + storage = new HulylakeStorage(baseUrl) + jest.clearAllMocks() + mockFetch.mockClear() + mockUploadXhr.mockClear() + }) + + describe('getFileUrl', () => { + it('should generate correct URL', () => { + const workspace = 'test-workspace' + const file = 'file-123' + + const url = storage.getFileUrl(workspace, file) + + expect(url).toBe(`${baseUrl}/api/${workspace}/${file}`) + }) + + it('should handle special characters in workspace and file names', () => { + const workspace = 'test workspace' + const file = 'file 123' + + const url = storage.getFileUrl(workspace, file) + + expect(url).toBe(`${baseUrl}/api/${workspace}/${file}`) + }) + + it('should handle base URL with trailing slash', () => { + const storageWithSlash = new HulylakeStorage('https://hulylake.example.com/') + const workspace = 'test-workspace' + const file = 'file-123' + + const url = storageWithSlash.getFileUrl(workspace, file) + + expect(url).toBe('https://hulylake.example.com/api/test-workspace/file-123') + }) + + it('should handle base URL without trailing slash', () => { + const storageWithoutSlash = new HulylakeStorage('https://hulylake.example.com') + const workspace = 'test-workspace' + const file = 'file-123' + + const url = storageWithoutSlash.getFileUrl(workspace, file) + + expect(url).toBe('https://hulylake.example.com/api/test-workspace/file-123') + }) + + it('should ignore filename parameter', () => { + const workspace = 'test-workspace' + const file = 'file-123' + const filename = 'ignored.txt' + + // The current implementation ignores the filename parameter + const url = storage.getFileUrl(workspace, file, filename) + + expect(url).toBe(`${baseUrl}/api/${workspace}/${file}`) + }) + }) + + describe('getFileMeta', () => { + it('should return empty object', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + const meta = await storage.getFileMeta(token, workspace, file) + + expect(meta).toEqual({}) + }) + }) + + describe('deleteFile', () => { + it('should delete file successfully', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200 + }) + + await storage.deleteFile(token, workspace, file) + + expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/${workspace}/${file}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}` + } + }) + }) + + it('should throw error when delete fails', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found' + }) + + await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Failed to delete file') + }) + + it('should handle network errors during delete', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const file = 'file-123' + + mockFetch.mockRejectedValueOnce(new Error('Network error')) + + await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Network error') + }) + }) + + describe('uploadFile', () => { + it('should upload file successfully with PUT method', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadXhr).toHaveBeenCalledWith( + { + url: `${baseUrl}/api/${workspace}/${uuid}`, + method: 'PUT', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': file.type, + 'Content-Length': file.size.toString() + }, + body: file + }, + undefined + ) + }) + + it('should upload file with progress tracking', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + const onProgress = jest.fn() + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file, { onProgress }) + + expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), { onProgress }) + }) + + it('should upload file with abort signal', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + const controller = new AbortController() + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file, { signal: controller.signal }) + + expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), { signal: controller.signal }) + }) + + it('should handle upload errors', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'file-uuid-123' + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + + mockUploadXhr.mockRejectedValueOnce(new Error('Upload failed')) + + await expect(storage.uploadFile(token, workspace, uuid, file)).rejects.toThrow('Upload failed') + }) + + it('should handle different file types', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'image-uuid-123' + const file = new File(['binary data'], 'image.png', { type: 'image/png' }) + + mockUploadXhr.mockResolvedValueOnce({ status: 201, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadXhr).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'PUT', + headers: expect.objectContaining({ + Authorization: `Bearer ${token}`, + 'Content-Type': 'image/png' + }), + body: file + }), + undefined + ) + }) + + it('should handle empty files', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'empty-uuid-123' + const file = new File([''], 'empty.txt', { type: 'text/plain' }) + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadXhr).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'PUT', + body: file + }), + undefined + ) + }) + }) + + describe('integration scenarios', () => { + it('should handle complete file lifecycle', async () => { + const token = 'test-token' + const workspace = 'test-workspace' + const uuid = 'lifecycle-uuid' + const filename = 'lifecycle.txt' + const file = new File(['test content'], filename, { type: 'text/plain' }) + + // Upload file + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + await storage.uploadFile(token, workspace, uuid, file) + + // Get file URL + const url = storage.getFileUrl(workspace, uuid, filename) + expect(url).toBe(`${baseUrl}/api/${workspace}/${uuid}`) + + // Get file meta (should return empty object for HulylakeStorage) + const meta = await storage.getFileMeta(token, workspace, uuid) + expect(meta).toEqual({}) + + // Delete file + mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) + await storage.deleteFile(token, workspace, uuid) + + expect(mockUploadXhr).toHaveBeenCalledTimes(1) + expect(mockFetch).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/foundations/core/packages/storage-client/src/__tests__/integration.test.ts b/foundations/core/packages/storage-client/src/__tests__/integration.test.ts new file mode 100644 index 0000000000..31817c5116 --- /dev/null +++ b/foundations/core/packages/storage-client/src/__tests__/integration.test.ts @@ -0,0 +1,499 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { createFileStorage, FileStorageConfig } from '../client' +import { FileStorage, FileStorageUploadOptions } from '../types' +import * as upload from '../upload' + +// Mock the upload module +jest.mock('../upload') +const mockUploadXhr = jest.mocked(upload.uploadXhr) +const mockUploadMultipart = jest.mocked(upload.uploadMultipart) + +// Mock fetch +const mockFetch = jest.fn() +global.fetch = mockFetch as any + +// Mock XMLHttpRequest +class MockXMLHttpRequest { + public url: string = '' + public method: string = '' + public headers: Record = {} + public body: any = null + public status: number = 200 + public statusText: string = 'OK' + public upload: { onprogress: ((event: ProgressEvent) => void) | null } = { onprogress: null } + public onload: (() => void) | null = null + public onerror: (() => void) | null = null + public onabort: (() => void) | null = null + public ontimeout: (() => void) | null = null + + open (method: string, url: string, async: boolean = true): void { + this.method = method + this.url = url + } + + setRequestHeader (key: string, value: string): void { + this.headers[key] = value + } + + send (body: any): void { + this.body = body + setTimeout(() => { + this.onload?.() + }, 0) + } + + abort (): void { + this.onabort?.() + } +} + +;(global as any).XMLHttpRequest = MockXMLHttpRequest + +beforeAll(() => {}) + +afterAll(() => {}) + +describe('Storage Client Integration Tests', () => { + beforeEach(() => { + jest.clearAllMocks() + mockFetch.mockClear() + mockUploadXhr.mockClear() + mockUploadMultipart.mockClear() + }) + + describe('Front Storage Integration', () => { + let storage: FileStorage + + beforeEach(() => { + const config: FileStorageConfig = { + uploadUrl: 'https://files.example.com' + } + storage = createFileStorage(config) + }) + + it('should complete full file lifecycle with Front storage', async () => { + const token = 'test-token' + const workspace = 'integration-workspace' + const uuid = 'integration-file-uuid' + const filename = 'integration-test.txt' + const file = new File(['Integration test content'], filename, { type: 'text/plain' }) + + // Mock successful upload + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + // Upload file + await storage.uploadFile(token, workspace, uuid, file) + + // Verify upload was called with correct parameters + expect(mockUploadXhr).toHaveBeenCalledWith( + { + url: 'https://files.example.com/integration-workspace', + method: 'POST', + headers: { Authorization: 'Bearer test-token' }, + body: expect.any(FormData) + }, + undefined + ) + + // Generate file URL + const fileUrl = storage.getFileUrl(workspace, uuid, filename) + expect(fileUrl).toBe(`https://files.example.com/${workspace}/${filename}?file=${uuid}&workspace=${workspace}`) + + // Get file metadata (should return empty object for Front storage) + const meta = await storage.getFileMeta(token, workspace, uuid) + expect(meta).toEqual({}) + + // Mock successful delete + mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) + + // Delete file + await storage.deleteFile(token, workspace, uuid) + + // Delete uses the file ID URL, not the filename URL + const deleteUrl = storage.getFileUrl(workspace, uuid) + expect(mockFetch).toHaveBeenCalledWith(deleteUrl, { + method: 'DELETE', + headers: { Authorization: 'Bearer test-token' } + }) + }) + + it('should handle upload with progress tracking', async () => { + const token = 'test-token' + const workspace = 'progress-workspace' + const uuid = 'progress-uuid' + const file = new File(['Progress test'], 'progress.txt', { type: 'text/plain' }) + const progressCallback = jest.fn() + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file, { + onProgress: progressCallback + }) + + expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), { onProgress: progressCallback }) + }) + + it('should handle upload with abort signal', async () => { + const token = 'test-token' + const workspace = 'abort-workspace' + const uuid = 'abort-uuid' + const file = new File(['Abort test'], 'abort.txt', { type: 'text/plain' }) + const controller = new AbortController() + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file, { + signal: controller.signal + }) + + expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), { signal: controller.signal }) + }) + }) + + describe('Datalake Storage Integration', () => { + let storage: FileStorage + + beforeEach(() => { + const config: FileStorageConfig = { + uploadUrl: 'https://files.example.com', + datalakeUrl: 'https://datalake.example.com' + } + storage = createFileStorage(config) + }) + + it('should complete full file lifecycle with small file', async () => { + const token = 'test-token' + const workspace = 'datalake-workspace' + const uuid = 'small-file-uuid' + const filename = 'small-file.txt' + const file = new File(['Small file content'], filename, { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 1024 }) // 1KB + + // Mock successful small file upload + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + // Upload small file (should use form-data) + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadXhr).toHaveBeenCalledWith( + { + url: `https://datalake.example.com/upload/form-data/${encodeURIComponent(workspace)}`, + method: 'POST', + headers: { Authorization: 'Bearer test-token' }, + body: expect.any(FormData) + }, + undefined + ) + + // Generate file URL + const fileUrl = storage.getFileUrl(workspace, uuid, filename) + expect(fileUrl).toBe(`https://datalake.example.com/blob/${workspace}/${uuid}/${filename}`) + + // Mock file metadata response + const expectedMeta = { size: 1024, contentType: 'text/plain' } + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(expectedMeta) + }) + + // Get file metadata + const meta = await storage.getFileMeta(token, workspace, uuid) + expect(meta).toEqual(expectedMeta) + + // Mock successful delete + mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) + + // Delete file + await storage.deleteFile(token, workspace, uuid) + + expect(mockFetch).toHaveBeenCalledWith(`https://datalake.example.com/blob/${workspace}/${uuid}`, { + method: 'DELETE', + headers: { Authorization: 'Bearer test-token' } + }) + }) + + it('should complete full file lifecycle with large file', async () => { + const token = 'test-token' + const workspace = 'datalake-workspace' + const uuid = 'large-file-uuid' + const filename = 'large-file.txt' + const file = new File(['Large file content'], filename, { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }) // 15MB + + // Mock successful multipart upload + mockUploadMultipart.mockResolvedValueOnce() + + // Upload large file (should use multipart) + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadMultipart).toHaveBeenCalledWith( + { + url: `https://datalake.example.com/upload/multipart/${encodeURIComponent(workspace)}/${encodeURIComponent(uuid)}`, + headers: { Authorization: 'Bearer test-token' }, + body: file + }, + undefined + ) + + expect(mockUploadXhr).not.toHaveBeenCalled() + + // Verify other operations work the same + const fileUrl = storage.getFileUrl(workspace, uuid, filename) + expect(fileUrl).toBe(`https://datalake.example.com/blob/${workspace}/${uuid}/${filename}`) + }) + + it('should handle file size threshold correctly', async () => { + const token = 'test-token' + const workspace = 'threshold-workspace' + + // Test exactly 10MB file (should use form-data) + const boundaryUuid = 'boundary-uuid' + const boundaryFile = new File(['boundary content'], 'boundary.txt', { type: 'text/plain' }) + Object.defineProperty(boundaryFile, 'size', { value: 10 * 1024 * 1024 }) // exactly 10MB + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + await storage.uploadFile(token, workspace, boundaryUuid, boundaryFile) + + expect(mockUploadXhr).toHaveBeenCalled() + expect(mockUploadMultipart).not.toHaveBeenCalled() + + // Reset mocks + mockUploadXhr.mockClear() + mockUploadMultipart.mockClear() + + // Test 10MB + 1 byte file (should use multipart) + const overBoundaryUuid = 'over-boundary-uuid' + const overBoundaryFile = new File(['over boundary content'], 'over-boundary.txt', { type: 'text/plain' }) + Object.defineProperty(overBoundaryFile, 'size', { value: 10 * 1024 * 1024 + 1 }) // 10MB + 1 byte + + mockUploadMultipart.mockResolvedValueOnce() + await storage.uploadFile(token, workspace, overBoundaryUuid, overBoundaryFile) + + expect(mockUploadMultipart).toHaveBeenCalled() + expect(mockUploadXhr).not.toHaveBeenCalled() + }) + }) + + describe('Hulylake Storage Integration', () => { + let storage: FileStorage + + beforeEach(() => { + const config: FileStorageConfig = { + uploadUrl: 'https://files.example.com', + hulylakeUrl: 'https://hulylake.example.com' + } + storage = createFileStorage(config) + }) + + it('should complete full file lifecycle with Hulylake storage', async () => { + const token = 'test-token' + const workspace = 'hulylake-workspace' + const uuid = 'hulylake-file-uuid' + const filename = 'hulylake-test.txt' + const file = new File(['Hulylake test content'], filename, { type: 'text/plain' }) + + // Mock successful upload + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + // Upload file (always uses PUT method) + await storage.uploadFile(token, workspace, uuid, file) + + expect(mockUploadXhr).toHaveBeenCalledWith( + { + url: `https://hulylake.example.com/api/${workspace}/${uuid}`, + method: 'PUT', + headers: { + Authorization: 'Bearer test-token', + 'Content-Type': 'text/plain', + 'Content-Length': file.size.toString() + }, + body: file + }, + undefined + ) + + // Generate file URL + const fileUrl = storage.getFileUrl(workspace, uuid, filename) + expect(fileUrl).toBe(`https://hulylake.example.com/api/${workspace}/${uuid}`) + + // Get file metadata (should return empty object for Hulylake storage) + const meta = await storage.getFileMeta(token, workspace, uuid) + expect(meta).toEqual({}) + + // Mock successful delete + mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) + + // Delete file + await storage.deleteFile(token, workspace, uuid) + + expect(mockFetch).toHaveBeenCalledWith(fileUrl, { + method: 'DELETE', + headers: { Authorization: 'Bearer test-token' } + }) + }) + }) + + describe('Error Handling Integration', () => { + it('should handle upload failures across all storage types', async () => { + const configs: Array<{ name: string, config: FileStorageConfig }> = [ + { name: 'Front', config: { uploadUrl: 'https://front.example.com' } }, + { + name: 'Datalake', + config: { uploadUrl: 'https://front.example.com', datalakeUrl: 'https://datalake.example.com' } + }, + { + name: 'Hulylake', + config: { uploadUrl: 'https://front.example.com', hulylakeUrl: 'https://hulylake.example.com' } + } + ] + + for (const { name, config } of configs) { + const storage = createFileStorage(config) + const token = 'test-token' + const workspace = `${name.toLowerCase()}-workspace` + const uuid = `${name.toLowerCase()}-uuid` + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + + // Mock upload failure + mockUploadXhr.mockRejectedValueOnce(new Error(`${name} upload failed`)) + mockUploadMultipart.mockRejectedValueOnce(new Error(`${name} multipart upload failed`)) + + await expect(storage.uploadFile(token, workspace, uuid, file)).rejects.toThrow( + new RegExp(`${name}.*upload failed`) + ) + + // Reset mocks for next iteration + mockUploadXhr.mockClear() + mockUploadMultipart.mockClear() + } + }) + + it('should handle delete failures across all storage types', async () => { + const configs: Array<{ name: string, config: FileStorageConfig }> = [ + { name: 'Front', config: { uploadUrl: 'https://front.example.com' } }, + { + name: 'Datalake', + config: { uploadUrl: 'https://front.example.com', datalakeUrl: 'https://datalake.example.com' } + }, + { + name: 'Hulylake', + config: { uploadUrl: 'https://front.example.com', hulylakeUrl: 'https://hulylake.example.com' } + } + ] + + for (const { name, config } of configs) { + const storage = createFileStorage(config) + const token = 'test-token' + const workspace = `${name.toLowerCase()}-workspace` + const uuid = `${name.toLowerCase()}-uuid` + + // Mock delete failure + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error' + }) + + await expect(storage.deleteFile(token, workspace, uuid)).rejects.toThrow('Failed to delete file') + + // Reset mocks for next iteration + mockFetch.mockClear() + } + }) + }) + + describe('Configuration Priority Integration', () => { + it('should respect storage priority in real usage scenarios', async () => { + // Test that Datalake takes priority over Hulylake + const config: FileStorageConfig = { + uploadUrl: 'https://upload.example.com', + datalakeUrl: 'https://datalake.example.com', + hulylakeUrl: 'https://hulylake.example.com' + } + + const storage = createFileStorage(config) + const token = 'test-token' + const workspace = 'priority-workspace' + const uuid = 'priority-uuid' + const file = new File(['priority test'], 'priority.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 1024 }) // Small file to ensure form-data upload + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + + await storage.uploadFile(token, workspace, uuid, file) + + // Should use Datalake form-data endpoint, not Hulylake PUT endpoint + expect(mockUploadXhr).toHaveBeenCalledWith( + expect.objectContaining({ + url: `https://datalake.example.com/upload/form-data/${encodeURIComponent(workspace)}`, + method: 'POST' // Datalake uses POST for form-data, Hulylake uses PUT + }), + undefined + ) + }) + }) + + describe('Upload Options Integration', () => { + it('should pass upload options correctly across all storage types', async () => { + const onProgress = jest.fn() + const controller = new AbortController() + const options: FileStorageUploadOptions = { + onProgress, + signal: controller.signal + } + + const configs: Array<{ name: string, config: FileStorageConfig }> = [ + { name: 'Front', config: { uploadUrl: 'https://front.example.com' } }, + { + name: 'Datalake', + config: { uploadUrl: 'https://front.example.com', datalakeUrl: 'https://datalake.example.com' } + }, + { + name: 'Hulylake', + config: { uploadUrl: 'https://front.example.com', hulylakeUrl: 'https://hulylake.example.com' } + } + ] + + for (const { name, config } of configs) { + const storage = createFileStorage(config) + const token = 'test-token' + const workspace = `${name.toLowerCase()}-options-workspace` + const uuid = `${name.toLowerCase()}-options-uuid` + const file = new File(['options test'], 'options.txt', { type: 'text/plain' }) + + if (name === 'Datalake') { + Object.defineProperty(file, 'size', { value: 1024 }) // Ensure small file for XHR upload + } + + mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) + mockUploadMultipart.mockResolvedValueOnce() + + await storage.uploadFile(token, workspace, uuid, file, options) + + if (name === 'Datalake' && file.size <= 10 * 1024 * 1024) { + expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), options) + } else if (name !== 'Datalake') { + expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), options) + } + + // Reset mocks for next iteration + mockUploadXhr.mockClear() + mockUploadMultipart.mockClear() + } + }) + }) +}) diff --git a/foundations/core/packages/storage-client/src/__tests__/upload.test.ts b/foundations/core/packages/storage-client/src/__tests__/upload.test.ts new file mode 100644 index 0000000000..198203c4e0 --- /dev/null +++ b/foundations/core/packages/storage-client/src/__tests__/upload.test.ts @@ -0,0 +1,704 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { uploadXhr, uploadMultipart, XHRUpload, MultipartUpload } from '../upload' + +// Mock XMLHttpRequest +class MockXMLHttpRequest { + public url: string = '' + public method: string = '' + public headers: Record = {} + public body: any = null + public status: number = 200 + public statusText: string = 'OK' + public responseText: string = '' + public upload: { onprogress: ((event: ProgressEvent) => void) | null } = { onprogress: null } + public onload: (() => void) | null = null + public onerror: (() => void) | null = null + public onabort: (() => void) | null = null + public ontimeout: (() => void) | null = null + + open (method: string, url: string, async: boolean = true): void { + this.method = method + this.url = url + } + + setRequestHeader (key: string, value: string): void { + this.headers[key] = value + } + + send (body: any): void { + this.body = body + // Don't auto-call onload - let tests control when it's called + } + + abort (): void { + this.onabort?.() + } + + // Test helpers + simulateProgress (loaded: number, total: number): void { + const event: Partial = { loaded, total, lengthComputable: true } + this.upload.onprogress?.(event as ProgressEvent) + } + + simulateError (): void { + this.onerror?.() + } + + simulateTimeout (): void { + this.ontimeout?.() + } + + simulateSuccess (responseText: string = ''): void { + this.responseText = responseText + this.onload?.() + } +} + +// Mock fetch +const mockFetch = jest.fn() +global.fetch = mockFetch as any + +// Track all XHR instances for multipart tests +const xhrInstances: MockXMLHttpRequest[] = [] + +describe('uploadXhr', () => { + let xhrInstance: MockXMLHttpRequest + + beforeEach(() => { + jest.clearAllMocks() + xhrInstances.length = 0 + xhrInstance = new MockXMLHttpRequest() + ;(global as any).XMLHttpRequest = jest.fn().mockImplementation(() => { + xhrInstances.push(xhrInstance) + return xhrInstance + }) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('should upload successfully with POST method', async () => { + const upload: XHRUpload = { + url: 'https://example.com/upload', + method: 'POST', + headers: { Authorization: 'Bearer token' }, + body: new FormData() + } + + const uploadPromise = uploadXhr(upload) + + // XHR setup is synchronous + expect(xhrInstance.method).toBe('POST') + expect(xhrInstance.url).toBe('https://example.com/upload') + expect(xhrInstance.headers.Authorization).toBe('Bearer token') + + // Manually trigger onload to resolve the promise with response + xhrInstance.simulateSuccess('{"result": "success"}') + + const result = await uploadPromise + expect(result.status).toBe(200) + expect(result.responseText).toBe('{"result": "success"}') + }) + + it('should upload successfully with PUT method', async () => { + const upload: XHRUpload = { + url: 'https://example.com/upload', + method: 'PUT', + headers: { 'Content-Type': 'application/octet-stream' }, + body: new Blob(['test content']) + } + + const uploadPromise = uploadXhr(upload) + + // XHR setup is synchronous + expect(xhrInstance.method).toBe('PUT') + expect(xhrInstance.headers['Content-Type']).toBe('application/octet-stream') + + // Manually trigger onload to resolve the promise + xhrInstance.simulateSuccess('') + + const result = await uploadPromise + expect(result.status).toBe(200) + expect(result.responseText).toBe('') + }) + + it('should track upload progress', async () => { + const onProgress = jest.fn() + const upload: XHRUpload = { + url: 'https://example.com/upload', + method: 'POST', + headers: {}, + body: new FormData() + } + + const uploadPromise = uploadXhr(upload, { onProgress }) + + // Simulate progress events (synchronous) + xhrInstance.simulateProgress(50, 100) + xhrInstance.simulateProgress(100, 100) + + // Manually trigger onload to resolve the promise + xhrInstance.simulateSuccess('') + + await uploadPromise + + expect(onProgress).toHaveBeenCalledWith({ + loaded: 50, + total: 100, + percentage: 50 + }) + expect(onProgress).toHaveBeenCalledWith({ + loaded: 100, + total: 100, + percentage: 100 + }) + }) + + it('should handle network errors', async () => { + const upload: XHRUpload = { + url: 'https://example.com/upload', + method: 'POST', + headers: {}, + body: new FormData() + } + + const uploadPromise = uploadXhr(upload) + + // Simulate error (synchronous) + xhrInstance.simulateError() + + await expect(uploadPromise).rejects.toThrow('Network error') + }) + + it('should handle HTTP errors', async () => { + xhrInstance.status = 500 + xhrInstance.statusText = 'Internal Server Error' + + const upload: XHRUpload = { + url: 'https://example.com/upload', + method: 'POST', + headers: {}, + body: new FormData() + } + + const uploadPromise = uploadXhr(upload) + + // Manually trigger onload to trigger the status check + xhrInstance.onload?.() + + await expect(uploadPromise).rejects.toThrow('Upload failed with status 500: Internal Server Error') + }) + + it('should handle timeout errors', async () => { + const upload: XHRUpload = { + url: 'https://example.com/upload', + method: 'POST', + headers: {}, + body: new FormData() + } + + const uploadPromise = uploadXhr(upload) + + // Simulate timeout (synchronous) + xhrInstance.simulateTimeout() + + await expect(uploadPromise).rejects.toThrow('Upload timeout') + }) + + it('should handle abort signal', async () => { + const controller = new AbortController() + const upload: XHRUpload = { + url: 'https://example.com/upload', + method: 'POST', + headers: {}, + body: new FormData() + } + + // Abort immediately + controller.abort() + + await expect(uploadXhr(upload, { signal: controller.signal })).rejects.toThrow('Upload aborted') + }) + + it.skip('should handle abort during upload', async () => { + // This test is complex due to async event handling in mocks + // The abort functionality is tested in integration tests + const controller = new AbortController() + const upload: XHRUpload = { + url: 'https://example.com/upload', + method: 'POST', + headers: {}, + body: new FormData() + } + + // Pre-abort the signal + controller.abort() + + // Should reject immediately if already aborted + await expect(uploadXhr(upload, { signal: controller.signal })).rejects.toThrow('Upload aborted') + }) +}) + +describe('uploadMultipart', () => { + beforeEach(() => { + jest.clearAllMocks() + mockFetch.mockClear() + mockFetch.mockReset() + xhrInstances.length = 0 + ;(global as any).XMLHttpRequest = jest.fn().mockImplementation(() => { + const instance = new MockXMLHttpRequest() + xhrInstances.push(instance) + return instance + }) + }) + + it('should upload small file in single chunk', async () => { + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 1024 }) // 1KB + + const upload: MultipartUpload = { + url: 'https://example.com/upload', + headers: { Authorization: 'Bearer token' }, + body: file + } + + // Mock multipart upload create and complete (using fetch) + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) + }) + .mockResolvedValueOnce({ + ok: true + }) + + const uploadPromise = uploadMultipart(upload) + + // Wait for async initialization to complete (fetch promise resolution) + await new Promise(setImmediate) + + // Should have 1 XHR for the part upload + expect(xhrInstances).toHaveLength(1) + const partXhr = xhrInstances[0] + + expect(partXhr.method).toBe('PUT') + expect(partXhr.url).toContain('/part') + expect(partXhr.url).toContain('uploadId=test-upload-id') + expect(partXhr.url).toContain('partNumber=1') + + // Simulate successful part upload + partXhr.simulateSuccess('{"etag": "test-etag-1"}') + + await uploadPromise + + // Check fetch was called for init and complete + expect(mockFetch).toHaveBeenCalledTimes(2) + + // Check initialization call + expect(mockFetch).toHaveBeenNthCalledWith(1, 'https://example.com/upload', { + method: 'POST', + headers: { + Authorization: 'Bearer token', + 'Content-Type': 'text/plain' + }, + signal: undefined + }) + + // Check completion call + expect(mockFetch).toHaveBeenNthCalledWith(2, expect.any(URL), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token' + }, + body: JSON.stringify({ parts: [{ partNumber: 1, etag: 'test-etag-1' }] }), + signal: undefined + }) + }) + + it('should upload large file in multiple chunks', async () => { + const file = new File(['test content'.repeat(1000000)], 'test.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 12 * 1024 * 1024 }) // 12MB + + const upload: MultipartUpload = { + url: 'https://example.com/upload', + headers: { Authorization: 'Bearer token' }, + body: file + } + + // Mock multipart upload create and complete + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) + }) + .mockResolvedValueOnce({ + ok: true + }) + + const uploadPromise = uploadMultipart(upload) + + // Wait for async initialization to complete + await new Promise(setImmediate) + + // Should have at least 1 XHR created for first part + expect(xhrInstances.length).toBeGreaterThan(0) + + // Simulate each part upload sequentially (12MB / 5MB = 3 parts) + for (let i = 0; i < 3; i++) { + // Wait for XHR to be created if not yet + while (xhrInstances[i] === undefined) { + await new Promise(setImmediate) + } + xhrInstances[i].simulateSuccess(`{"etag": "test-etag-${i + 1}"}`) + // Allow upload to process the completion + await new Promise(setImmediate) + } + + await uploadPromise + + // Should be 1 init + 1 complete = 2 fetch calls (parts use XHR) + expect(mockFetch).toHaveBeenCalledTimes(2) + // Should have 3 XHR instances for the 3 parts + expect(xhrInstances).toHaveLength(3) + }) + + it('should track progress during multipart upload', async () => { + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 10 * 1024 * 1024 }) // 10MB + + const onProgress = jest.fn() + const upload: MultipartUpload = { + url: 'https://example.com/upload', + headers: { + Authorization: 'Bearer token', + 'Content-Type': 'text/plain' + }, + body: file + } + + // Mock multipart upload create and complete + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) + }) + .mockResolvedValueOnce({ + ok: true + }) + + const uploadPromise = uploadMultipart(upload, { onProgress }) + + // Wait for async initialization to complete + await new Promise(setImmediate) + + // Simulate progress on first part (should be 2 parts: 5MB + 5MB) + if (xhrInstances[0] !== undefined) { + xhrInstances[0].simulateProgress(2.5 * 1024 * 1024, 5 * 1024 * 1024) // 2.5MB of 5MB + xhrInstances[0].simulateProgress(5 * 1024 * 1024, 5 * 1024 * 1024) // Complete first part + xhrInstances[0].simulateSuccess('{"etag": "test-etag-1"}') + } + + // Wait for second XHR to be created + await new Promise(setImmediate) + + // Simulate second part + if (xhrInstances[1] !== undefined) { + xhrInstances[1].simulateProgress(2.5 * 1024 * 1024, 5 * 1024 * 1024) // 2.5MB of 5MB (second part) + xhrInstances[1].simulateProgress(5 * 1024 * 1024, 5 * 1024 * 1024) // Complete second part + xhrInstances[1].simulateSuccess('{"etag": "test-etag-2"}') + } + + await uploadPromise + + expect(onProgress).toHaveBeenCalled() + const progressCalls = onProgress.mock.calls + + // Check that progress was reported with proper values + expect(progressCalls.length).toBeGreaterThan(0) + + // First progress should show partial progress of first chunk + const firstCall = progressCalls[0][0] + expect(firstCall).toHaveProperty('loaded') + expect(firstCall).toHaveProperty('total', 10 * 1024 * 1024) + expect(firstCall).toHaveProperty('percentage') + expect(firstCall.loaded).toBeLessThanOrEqual(firstCall.total) + + // Progress should increase over time + const lastCall = progressCalls[progressCalls.length - 1][0] + expect(lastCall.loaded).toBeGreaterThanOrEqual(firstCall.loaded) + }) + + it('should handle initialization failure', async () => { + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + + const upload: MultipartUpload = { + url: 'https://example.com/upload', + headers: { + Authorization: 'Bearer token', + 'Content-Type': 'text/plain' + }, + body: file + } + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 401 + }) + + await expect(uploadMultipart(upload)).rejects.toThrow('Failed to initialize multipart upload') + }) + + it('should handle part upload failure', async () => { + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 6 * 1024 * 1024 }) // 6MB + + const upload: MultipartUpload = { + url: 'https://example.com/upload', + headers: { Authorization: 'Bearer token' }, + body: file + } + + // Mock successful initialization and abort call + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) + }) + .mockResolvedValueOnce({ + ok: true // abort call + }) + + const uploadPromise = uploadMultipart(upload) + + // Wait for async initialization and XHR to be created + await new Promise(setImmediate) + + // Simulate upload failure + if (xhrInstances[0] !== undefined) { + xhrInstances[0].status = 500 + xhrInstances[0].statusText = 'Internal Server Error' + xhrInstances[0].onload?.() + } + + await expect(uploadPromise).rejects.toThrow('Upload failed with status 500') + }) + + it('should handle completion failure', async () => { + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 1024 }) // 1KB + + const upload: MultipartUpload = { + url: 'https://example.com/upload', + headers: { Authorization: 'Bearer token' }, + body: file + } + + // Mock successful init, failed completion, and abort + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) + }) + .mockResolvedValueOnce({ + ok: false, + status: 500 + }) + .mockResolvedValueOnce({ + ok: true // abort call + }) + + const uploadPromise = uploadMultipart(upload) + + // Wait for async initialization and XHR to be created + await new Promise(setImmediate) + + if (xhrInstances[0] !== undefined) { + xhrInstances[0].simulateSuccess('{"etag": "test-etag-1"}') + } + + await expect(uploadPromise).rejects.toThrow('Failed to complete multipart upload') + }) + + it('should handle abort signal', async () => { + const controller = new AbortController() + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + + const upload: MultipartUpload = { + url: 'https://example.com/upload', + headers: { Authorization: 'Bearer token' }, + body: file + } + + // Mock the initialization call + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) + }) + + // Abort immediately - this should be caught before part upload + controller.abort() + + // The implementation checks for abort before each part upload + await expect(uploadMultipart(upload, { signal: controller.signal })).rejects.toThrow('Upload aborted') + }) + + it('should report immediate progress updates during part upload', async () => { + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 5 * 1024 * 1024 }) // 5MB + + const progressUpdates: Array<{ loaded: number, total: number, percentage: number }> = [] + const onProgress = jest.fn((progress) => { + progressUpdates.push({ ...progress }) + }) + + const upload: MultipartUpload = { + url: 'https://example.com/upload', + headers: { Authorization: 'Bearer token' }, + body: file + } + + // Mock multipart upload create and complete + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) + }) + .mockResolvedValueOnce({ + ok: true + }) + + const uploadPromise = uploadMultipart(upload, { onProgress }) + + // Wait for async initialization to complete + await new Promise(setImmediate) + + // Simulate incremental progress updates (this is the key feature we're testing) + if (xhrInstances[0] !== undefined) { + const chunkSize = 5 * 1024 * 1024 + xhrInstances[0].simulateProgress(1 * 1024 * 1024, chunkSize) // 20% of chunk + xhrInstances[0].simulateProgress(2 * 1024 * 1024, chunkSize) // 40% of chunk + xhrInstances[0].simulateProgress(3 * 1024 * 1024, chunkSize) // 60% of chunk + xhrInstances[0].simulateProgress(4 * 1024 * 1024, chunkSize) // 80% of chunk + xhrInstances[0].simulateProgress(5 * 1024 * 1024, chunkSize) // 100% of chunk + xhrInstances[0].simulateSuccess('{"etag": "test-etag-1"}') + } + + await uploadPromise + + // Verify we got multiple intermediate progress updates + expect(progressUpdates.length).toBeGreaterThanOrEqual(5) + + // Verify progress is monotonically increasing + for (let i = 1; i < progressUpdates.length; i++) { + expect(progressUpdates[i].loaded).toBeGreaterThanOrEqual(progressUpdates[i - 1].loaded) + } + + // Verify percentages are calculated correctly + expect(progressUpdates[0].percentage).toBe(20) + expect(progressUpdates[1].percentage).toBe(40) + expect(progressUpdates[2].percentage).toBe(60) + expect(progressUpdates[3].percentage).toBe(80) + expect(progressUpdates[4].percentage).toBe(100) + + // Verify total is consistent + progressUpdates.forEach((update) => { + expect(update.total).toBe(5 * 1024 * 1024) + }) + }) + + it('should correctly calculate progress across multiple chunks', async () => { + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) + Object.defineProperty(file, 'size', { value: 10 * 1024 * 1024 }) // 10MB = 2 chunks + + const progressUpdates: Array<{ loaded: number, total: number, percentage: number }> = [] + const onProgress = jest.fn((progress) => { + progressUpdates.push({ ...progress }) + }) + + const upload: MultipartUpload = { + url: 'https://example.com/upload', + headers: { Authorization: 'Bearer token' }, + body: file + } + + // Mock multipart upload create and complete + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) + }) + .mockResolvedValueOnce({ + ok: true + }) + + const uploadPromise = uploadMultipart(upload, { onProgress }) + + // Wait for async initialization to complete + await new Promise(setImmediate) + + // Simulate first chunk progress (5MB) + if (xhrInstances[0] !== undefined) { + xhrInstances[0].simulateProgress(2.5 * 1024 * 1024, 5 * 1024 * 1024) // 25% overall + xhrInstances[0].simulateProgress(5 * 1024 * 1024, 5 * 1024 * 1024) // 50% overall + xhrInstances[0].simulateSuccess('{"etag": "test-etag-1"}') + } + + // Wait for second XHR to be created + await new Promise(setImmediate) + + // Simulate second chunk progress (5MB) + if (xhrInstances[1] !== undefined) { + xhrInstances[1].simulateProgress(2.5 * 1024 * 1024, 5 * 1024 * 1024) // 75% overall + xhrInstances[1].simulateProgress(5 * 1024 * 1024, 5 * 1024 * 1024) // 100% overall + xhrInstances[1].simulateSuccess('{"etag": "test-etag-2"}') + } + + await uploadPromise + + // Verify we got progress updates + expect(progressUpdates.length).toBeGreaterThan(0) + + // Find progress updates at key milestones (from first chunk) + const progress25 = progressUpdates.find((p) => p.percentage === 25) + const progress50 = progressUpdates.find((p) => p.percentage === 50) + + // At least first chunk progress should be reported + expect(progress25).toBeDefined() + expect(progress50).toBeDefined() + + // Verify the loaded values are correct for milestones that exist + if (progress25 !== undefined) { + expect(progress25.loaded).toBe(2.5 * 1024 * 1024) + expect(progress25.total).toBe(10 * 1024 * 1024) + } + if (progress50 !== undefined) { + expect(progress50.loaded).toBe(5 * 1024 * 1024) + expect(progress50.total).toBe(10 * 1024 * 1024) + } + + // Verify that progress updates span across the upload + const progressValues = progressUpdates.map((p) => p.loaded) + const minProgress = Math.min(...progressValues) + const maxProgress = Math.max(...progressValues) + + // Should have progress from start through at least first chunk + expect(minProgress).toBeGreaterThan(0) + expect(maxProgress).toBeGreaterThanOrEqual(2.5 * 1024 * 1024) + }) +}) diff --git a/foundations/core/packages/storage-client/src/client/datalake.ts b/foundations/core/packages/storage-client/src/client/datalake.ts new file mode 100644 index 0000000000..24c9476d13 --- /dev/null +++ b/foundations/core/packages/storage-client/src/client/datalake.ts @@ -0,0 +1,88 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { concatLink } from '@hcengineering/core' +import { FileStorage, FileStorageUploadOptions } from '../types' +import { uploadMultipart, uploadXhr } from '../upload' + +/** @public */ +export class DatalakeStorage implements FileStorage { + constructor (private readonly baseUrl: string) {} + + getFileUrl (workspace: string, file: string, filename?: string): string { + const path = filename !== undefined ? `/blob/${workspace}/${file}/${filename}` : `/blob/${workspace}/${file}` + return concatLink(this.baseUrl, path) + } + + async getFileMeta (token: string, workspace: string, file: string): Promise> { + const url = concatLink(this.baseUrl, `/meta/${encodeURIComponent(workspace)}/${encodeURIComponent(file)}`) + try { + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${token}` + } + }) + if (response.ok) { + return await response.json() + } + } catch (err: any) {} + return {} + } + + async deleteFile (token: string, workspace: string, file: string): Promise { + const url = this.getFileUrl(workspace, file) + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}` + } + }) + + if (!response.ok) { + throw new Error('Failed to delete file') + } + } + + async uploadFile ( + token: string, + workspace: string, + uuid: string, + file: File, + options?: FileStorageUploadOptions + ): Promise { + if (file.size <= 10 * 1024 * 1024) { + const formData = new FormData() + formData.append('file', file, uuid) + + await uploadXhr( + { + url: concatLink(this.baseUrl, `/upload/form-data/${encodeURIComponent(workspace)}`), + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + body: formData + }, + options + ) + } else { + const url = concatLink( + this.baseUrl, + `/upload/multipart/${encodeURIComponent(workspace)}/${encodeURIComponent(uuid)}` + ) + const headers = { Authorization: `Bearer ${token}` } + await uploadMultipart({ url, headers, body: file }, options) + } + } +} diff --git a/foundations/core/packages/storage-client/src/client/front.ts b/foundations/core/packages/storage-client/src/client/front.ts new file mode 100644 index 0000000000..67ade0229c --- /dev/null +++ b/foundations/core/packages/storage-client/src/client/front.ts @@ -0,0 +1,68 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { concatLink } from '@hcengineering/core' +import { FileStorage, FileStorageUploadOptions } from '../types' +import { uploadXhr } from '../upload' + +/** @public */ +export class FrontStorage implements FileStorage { + constructor (private readonly baseUrl: string) {} + + getFileUrl (workspace: string, file: string, filename?: string): string { + const path = `/${workspace}/${filename ?? file}?file=${file}&workspace=${workspace}` + return concatLink(this.baseUrl, path) + } + + async getFileMeta (token: string, workspace: string, file: string): Promise> { + return {} + } + + async deleteFile (token: string, workspace: string, file: string): Promise { + const url = this.getFileUrl(workspace, file) + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}` + } + }) + + if (!response.ok) { + throw new Error('Failed to delete file') + } + } + + async uploadFile ( + token: string, + workspace: string, + uuid: string, + file: File, + options?: FileStorageUploadOptions + ): Promise { + const formData = new FormData() + formData.append('file', file, uuid) + + await uploadXhr( + { + url: concatLink(this.baseUrl, `/${workspace}`), + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + body: formData + }, + options + ) + } +} diff --git a/foundations/core/packages/storage-client/src/client/hulylake.ts b/foundations/core/packages/storage-client/src/client/hulylake.ts new file mode 100644 index 0000000000..397815c015 --- /dev/null +++ b/foundations/core/packages/storage-client/src/client/hulylake.ts @@ -0,0 +1,71 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { concatLink } from '@hcengineering/core' +import { FileStorage, FileStorageUploadOptions } from '../types' +import { uploadXhr } from '../upload' + +/** @public */ +export class HulylakeStorage implements FileStorage { + constructor (private readonly baseUrl: string) {} + + getFileUrl (workspace: string, file: string, filename?: string): string { + const path = `/api/${workspace}/${file}` + return concatLink(this.baseUrl, path) + } + + async getFileMeta (token: string, workspace: string, file: string): Promise> { + return {} + } + + async deleteFile (token: string, workspace: string, file: string): Promise { + const url = this.getFileUrl(workspace, file) + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}` + } + }) + + if (!response.ok) { + throw new Error('Failed to delete file') + } + } + + async uploadFile ( + token: string, + workspace: string, + uuid: string, + file: File, + options?: FileStorageUploadOptions + ): Promise { + const url = this.getFileUrl(workspace, uuid) + + await uploadXhr( + { + url, + method: 'PUT', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': file.type, + 'Content-Length': file.size.toString() + }, + body: file + }, + options + ) + } +} diff --git a/foundations/core/packages/storage-client/src/client/index.ts b/foundations/core/packages/storage-client/src/client/index.ts new file mode 100644 index 0000000000..4ee30a2b8f --- /dev/null +++ b/foundations/core/packages/storage-client/src/client/index.ts @@ -0,0 +1,45 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { DatalakeStorage } from './datalake' +import { FrontStorage } from './front' +import { HulylakeStorage } from './hulylake' + +import { FileStorage } from '../types' + +/** @public */ +export interface FileStorageConfig { + uploadUrl: string + datalakeUrl?: string + hulylakeUrl?: string +} + +/** @public */ +export function createFileStorage (config: FileStorageConfig): FileStorage { + const { uploadUrl, datalakeUrl, hulylakeUrl } = config + + if (datalakeUrl !== undefined && datalakeUrl !== '') { + console.debug('Using Datalake storage') + return new DatalakeStorage(datalakeUrl) + } + + if (hulylakeUrl !== undefined && hulylakeUrl !== '') { + console.debug('Using Hulylake storage') + return new HulylakeStorage(hulylakeUrl) + } + + console.debug('Using Front storage') + return new FrontStorage(uploadUrl) +} diff --git a/foundations/core/packages/storage-client/src/index.ts b/foundations/core/packages/storage-client/src/index.ts new file mode 100644 index 0000000000..0f078ded71 --- /dev/null +++ b/foundations/core/packages/storage-client/src/index.ts @@ -0,0 +1,17 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './client' +export * from './types' diff --git a/foundations/core/packages/storage-client/src/types.ts b/foundations/core/packages/storage-client/src/types.ts new file mode 100644 index 0000000000..bcbe88be8f --- /dev/null +++ b/foundations/core/packages/storage-client/src/types.ts @@ -0,0 +1,41 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/** @public */ +export interface FileStorageUploadProgress { + loaded: number + total: number + percentage: number +} + +/** @public */ +export interface FileStorageUploadOptions { + onProgress?: (progress: FileStorageUploadProgress) => void + signal?: AbortSignal +} + +/** @public */ +export interface FileStorage { + getFileUrl: (workspace: string, file: string, filename?: string) => string + getFileMeta: (token: string, workspace: string, file: string) => Promise> + uploadFile: ( + token: string, + workspace: string, + uuid: string, + file: File, + options?: FileStorageUploadOptions + ) => Promise + deleteFile: (token: string, workspace: string, file: string) => Promise +} diff --git a/foundations/core/packages/storage-client/src/upload.ts b/foundations/core/packages/storage-client/src/upload.ts new file mode 100644 index 0000000000..9272c9b6a9 --- /dev/null +++ b/foundations/core/packages/storage-client/src/upload.ts @@ -0,0 +1,258 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { concatLink } from '@hcengineering/core' +import { FileStorageUploadOptions } from './types' + +/** @public */ +export interface XHRUpload { + url: string + method: 'POST' | 'PUT' + headers: Record + body: XMLHttpRequestBodyInit +} + +/** @public */ +export interface XHRUploadResult { + status: number + responseText: string +} + +/** @public */ +export async function uploadXhr (upload: XHRUpload, options?: FileStorageUploadOptions): Promise { + const signal = options?.signal + const onProgress = options?.onProgress + + // Check if already aborted before starting + if (signal?.aborted === true) { + throw new Error('Upload aborted') + } + + const xhr = new XMLHttpRequest() + + const abortHandler = (): void => { + xhr.abort() + } + + if (signal !== undefined) { + signal.addEventListener('abort', abortHandler) + } + + try { + return await new Promise((resolve, reject) => { + xhr.upload.onprogress = (event) => { + if (event.lengthComputable) { + onProgress?.({ + loaded: event.loaded, + total: event.total, + percentage: Math.round((100 * event.loaded) / event.total) + }) + } + } + + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve({ + status: xhr.status, + responseText: xhr.responseText + }) + } else { + reject(new Error(`Upload failed with status ${xhr.status}: ${xhr.statusText}`)) + } + } + + xhr.onerror = () => { + reject(new Error('Network error')) + } + + xhr.onabort = () => { + reject(new Error('Upload aborted')) + } + + xhr.ontimeout = () => { + reject(new Error('Upload timeout')) + } + + xhr.open(upload.method, upload.url, true) + + for (const key in upload.headers) { + xhr.setRequestHeader(key, upload.headers[key]) + } + + xhr.send(upload.body) + }) + } finally { + if (signal !== undefined) { + signal.removeEventListener('abort', abortHandler) + } + } +} + +/** @public */ +export interface MultipartUpload { + url: string + headers: Record + body: File | Blob +} + +/** @public */ +export async function uploadMultipart (upload: MultipartUpload, options?: FileStorageUploadOptions): Promise { + const CHUNK_SIZE = 5 * 1024 * 1024 // 5MB chunks + const { url, headers, body } = upload + const signal = options?.signal + const onProgress = options?.onProgress + + let uploadId: string | undefined + + try { + const { uploadId } = await multipartUploadCreate(url, { ...headers, 'Content-Type': body.type }, signal) + + const parts: Array<{ partNumber: number, etag: string }> = [] + const totalParts = Math.ceil(body.size / CHUNK_SIZE) + let uploaded = 0 + + for (let partNumber = 1; partNumber <= totalParts; partNumber++) { + const start = (partNumber - 1) * CHUNK_SIZE + const end = Math.min(start + CHUNK_SIZE, body.size) + const chunk = body.slice(start, end) + + throwIfAborted(signal) + + const partOptions: FileStorageUploadOptions = { + signal, + onProgress: + onProgress !== undefined + ? (progress) => { + const loaded = uploaded + progress.loaded + onProgress({ + loaded, + total: body.size, + percentage: Math.round((loaded * 100) / body.size) + }) + } + : undefined + } + + const { etag } = await multipartUploadPart(url, headers, uploadId, partNumber, chunk, partOptions) + parts.push({ partNumber, etag }) + + uploaded += chunk.size + } + + throwIfAborted(signal) + + await multipartUploadComplete(url, headers, uploadId, parts, signal) + } catch (err) { + if (uploadId !== undefined) { + await multipartUploadAbort(url, headers, uploadId) + } + + throw err instanceof Error ? err : new Error(String(err)) + } +} + +function throwIfAborted (signal?: AbortSignal): void { + if (signal?.aborted === true) { + throw new Error('Upload aborted') + } +} + +async function multipartUploadCreate ( + baseUrl: string, + headers: Record, + signal?: AbortSignal +): Promise<{ uuid: string, uploadId: string }> { + const response = await fetch(baseUrl, { + signal, + method: 'POST', + headers + }) + + if (!response.ok) { + throw new Error('Failed to initialize multipart upload') + } + + const { uuid, uploadId } = await response.json() + return { uuid, uploadId } +} + +async function multipartUploadComplete ( + baseUrl: string, + headers: Record, + uploadId: string, + parts: Array<{ partNumber: number, etag: string }>, + signal?: AbortSignal +): Promise { + const url = new URL(concatLink(baseUrl, '/complete')) + url.searchParams.set('uploadId', uploadId) + + const response = await fetch(url, { + signal, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...headers + }, + body: JSON.stringify({ parts }) + }) + + if (!response.ok) { + throw new Error('Failed to complete multipart upload') + } +} + +async function multipartUploadPart ( + baseUrl: string, + headers: Record, + uploadId: string, + partNumber: number, + blob: Blob, + options?: FileStorageUploadOptions +): Promise<{ etag: string }> { + const url = new URL(concatLink(baseUrl, '/part')) + url.searchParams.set('uploadId', uploadId) + url.searchParams.set('partNumber', `${partNumber}`) + + const result = await uploadXhr( + { + url: url.toString(), + method: 'PUT', + headers, + body: blob + }, + options + ) + + try { + const response = JSON.parse(result.responseText) + return { etag: response.etag } + } catch (err) { + throw new Error(`Failed to parse response for part ${partNumber}`) + } +} + +async function multipartUploadAbort (baseUrl: string, headers: Record, uploadId: string): Promise { + const url = new URL(concatLink(baseUrl, '/abort')) + url.searchParams.set('uploadId', uploadId) + + const response = await fetch(url, { + method: 'POST', + headers + }) + + if (!response.ok) { + throw new Error('Failed to reject multipart upload') + } +} diff --git a/foundations/core/packages/storage-client/tsconfig.json b/foundations/core/packages/storage-client/tsconfig.json new file mode 100644 index 0000000000..a0f3d087e1 --- /dev/null +++ b/foundations/core/packages/storage-client/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo", + "types": ["node", "jest"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/storage/.eslintrc.js b/foundations/core/packages/storage/.eslintrc.js new file mode 100644 index 0000000000..ce90fb9646 --- /dev/null +++ b/foundations/core/packages/storage/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/storage/.npmignore b/foundations/core/packages/storage/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/storage/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/storage/CHANGELOG.json b/foundations/core/packages/storage/CHANGELOG.json new file mode 100644 index 0000000000..6e836ef1f5 --- /dev/null +++ b/foundations/core/packages/storage/CHANGELOG.json @@ -0,0 +1,69 @@ +{ + "name": "@hcengineering/storage", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/storage_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/storage_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/storage_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/storage_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/storage/CHANGELOG.md b/foundations/core/packages/storage/CHANGELOG.md new file mode 100644 index 0000000000..21264a95a6 --- /dev/null +++ b/foundations/core/packages/storage/CHANGELOG.md @@ -0,0 +1,28 @@ +# Change Log - @hcengineering/storage + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/storage/config/rig.json b/foundations/core/packages/storage/config/rig.json new file mode 100644 index 0000000000..78cc5a1733 --- /dev/null +++ b/foundations/core/packages/storage/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "node" +} diff --git a/foundations/core/packages/storage/jest.config.js b/foundations/core/packages/storage/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/storage/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/storage/package.json b/foundations/core/packages/storage/package.json new file mode 100644 index 0000000000..be13fe5431 --- /dev/null +++ b/foundations/core/packages/storage/package.json @@ -0,0 +1,62 @@ +{ + "name": "@hcengineering/storage", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Anticrm Platform Contributors", + "template": "@hcengineering/node-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent --forceExit --coverage", + "build:watch": "compile", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --forceExit --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/node": "^22.18.1", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/platform": "workspace:^0.7.18", + "@hcengineering/core": "workspace:^0.7.22", + "fast-equals": "^5.2.2" + }, + "files": [ + "lib/**/*", + "!lib/**/__test__/**", + "types/**/*", + "!types/**/__test__/**", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + } +} diff --git a/foundations/core/packages/storage/src/index.ts b/foundations/core/packages/storage/src/index.ts new file mode 100644 index 0000000000..255680bb64 --- /dev/null +++ b/foundations/core/packages/storage/src/index.ts @@ -0,0 +1,233 @@ +// +// Copyright © 2024 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type WorkspaceIds, + type Blob, + type MeasureContext, + type StorageIterator, + type WorkspaceDataId +} from '@hcengineering/core' +import { PlatformError, unknownError } from '@hcengineering/platform' +import { type Readable } from 'stream' + +export type ListBlobResult = Omit & { contentType?: string } +export interface UploadedObjectInfo { + etag: string + versionId: string | null +} + +export interface BlobStorageIterator { + next: () => Promise + close: () => Promise +} + +export interface BucketInfo { + name: string + delete: () => Promise + list: () => Promise +} + +export interface StorageAdapter { + initialize: (ctx: MeasureContext, wsIds: WorkspaceIds) => Promise + + close: () => Promise + + exists: (ctx: MeasureContext, wsIds: WorkspaceIds) => Promise + make: (ctx: MeasureContext, wsIds: WorkspaceIds) => Promise + delete: (ctx: MeasureContext, wsIds: WorkspaceIds) => Promise + + listBuckets: (ctx: MeasureContext) => Promise + remove: (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]) => Promise + listStream: (ctx: MeasureContext, wsIds: WorkspaceIds) => Promise + stat: (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string) => Promise + get: (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string) => Promise + put: ( + ctx: MeasureContext, + wsIds: WorkspaceIds, + objectName: string, + stream: Readable | Buffer | string, + contentType: string, + size?: number + ) => Promise + read: (ctx: MeasureContext, wsIds: WorkspaceIds, name: string) => Promise + partial: ( + ctx: MeasureContext, + wsIds: WorkspaceIds, + objectName: string, + offset: number, + length?: number + ) => Promise + + getUrl: (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string) => Promise +} + +export interface NamedStorageAdapter { + name: string + adapter: StorageAdapter +} + +export interface StorageAdapterEx extends StorageAdapter { + adapters?: NamedStorageAdapter[] + + find: (ctx: MeasureContext, wsIds: WorkspaceIds) => StorageIterator +} + +/** + * Ad dummy storage adapter for tests + */ +export class DummyStorageAdapter implements StorageAdapter, StorageAdapterEx { + defaultAdapter: string = '' + async syncBlobFromStorage (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { + throw new PlatformError(unknownError('Method not implemented')) + } + + async initialize (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} + + async close (): Promise {} + + async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { + return false + } + + find (ctx: MeasureContext, wsIds: WorkspaceIds): StorageIterator { + return { + next: async (ctx) => [], + close: async (ctx) => {} + } + } + + async listBuckets (ctx: MeasureContext): Promise { + return [] + } + + async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} + + async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} + + async remove (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]): Promise {} + + async list (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { + return [] + } + + async listStream (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { + return { + next: async (): Promise => { + return [] + }, + close: async () => {} + } + } + + async stat (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise { + return undefined + } + + async get (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise { + throw new Error('not implemented') + } + + async partial ( + ctx: MeasureContext, + wsIds: WorkspaceIds, + objectName: string, + offset: number, + length?: number | undefined + ): Promise { + throw new Error('not implemented') + } + + async read (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise { + throw new Error('not implemented') + } + + async put ( + ctx: MeasureContext, + wsIds: WorkspaceIds, + objectName: string, + stream: string | Readable | Buffer, + contentType: string, + size?: number | undefined + ): Promise { + throw new Error('not implemented') + } + + async getUrl (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { + throw new Error('not implemented') + } +} + +export function createDummyStorageAdapter (): StorageAdapter { + return new DummyStorageAdapter() +} + +export async function removeAllObjects ( + ctx: MeasureContext, + storage: StorageAdapter, + wsIds: WorkspaceIds +): Promise { + ctx.warn('removing all objects from workspace', wsIds) + // We need to list all files and delete them + const iterator = await storage.listStream(ctx, wsIds) + try { + let bulk: string[] = [] + while (true) { + const objs = await iterator.next() + if (objs.length === 0) { + break + } + for (const obj of objs) { + bulk.push(obj._id) + if (bulk.length > 50) { + await storage.remove(ctx, wsIds, bulk) + bulk = [] + } + } + } + if (bulk.length > 0) { + await storage.remove(ctx, wsIds, bulk) + bulk = [] + } + } finally { + await iterator.close() + } +} + +export async function objectsToArray ( + ctx: MeasureContext, + storage: StorageAdapter, + wsIds: WorkspaceIds +): Promise { + // We need to list all files and delete them + const iterator = await storage.listStream(ctx, wsIds) + try { + const bulk: ListBlobResult[] = [] + while (true) { + const obj = await iterator.next() + if (obj.length === 0) { + break + } + bulk.push(...obj) + } + return bulk + } finally { + await iterator.close() + } +} + +export function getDataId (wsIds: WorkspaceIds): WorkspaceDataId { + return wsIds.dataId ?? (wsIds.uuid as unknown as WorkspaceDataId) +} diff --git a/foundations/core/packages/storage/tsconfig.json b/foundations/core/packages/storage/tsconfig.json new file mode 100644 index 0000000000..c6a877cf6c --- /dev/null +++ b/foundations/core/packages/storage/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/text-core/.eslintrc.js b/foundations/core/packages/text-core/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/text-core/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/text-core/CHANGELOG.json b/foundations/core/packages/text-core/CHANGELOG.json new file mode 100644 index 0000000000..e6414f8cc7 --- /dev/null +++ b/foundations/core/packages/text-core/CHANGELOG.json @@ -0,0 +1,75 @@ +{ + "name": "@hcengineering/text-core", + "entries": [ + { + "version": "0.7.18", + "tag": "@hcengineering/text-core_v0.7.18", + "date": "Mon, 27 Oct 2025 16:46:51 GMT", + "comments": { + "patch": [ + { + "comment": "add support for textColor and textStyle marks" + } + ] + } + }, + { + "version": "0.7.17", + "tag": "@hcengineering/text-core_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/text-core_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/text-core_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/text-core_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/text-core/CHANGELOG.md b/foundations/core/packages/text-core/CHANGELOG.md new file mode 100644 index 0000000000..1c5c41a8c8 --- /dev/null +++ b/foundations/core/packages/text-core/CHANGELOG.md @@ -0,0 +1,35 @@ +# Change Log - @hcengineering/text-core + +This log was last generated on Mon, 27 Oct 2025 16:46:51 GMT and should not be manually modified. + +## 0.7.18 +Mon, 27 Oct 2025 16:46:51 GMT + +### Patches + +- add support for textColor and textStyle marks + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/text-core/config/rig.json b/foundations/core/packages/text-core/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/text-core/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/text-core/jest.config.js b/foundations/core/packages/text-core/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/text-core/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/text-core/package.json b/foundations/core/packages/text-core/package.json new file mode 100644 index 0000000000..d9db61d2ee --- /dev/null +++ b/foundations/core/packages/text-core/package.json @@ -0,0 +1,60 @@ +{ + "name": "@hcengineering/text-core", + "version": "0.7.18", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "build:watch": "compile", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/markdown-it": "~13.0.0", + "jest-environment-jsdom": "^30.2.0", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22", + "fast-equals": "^5.2.2", + "hash-it": "^6.0.0" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/text-core/src/index.ts b/foundations/core/packages/text-core/src/index.ts new file mode 100644 index 0000000000..5e2540fdb2 --- /dev/null +++ b/foundations/core/packages/text-core/src/index.ts @@ -0,0 +1,20 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './markup/dsl' +export * from './markup/model' +export * from './markup/reference' +export * from './markup/traverse' +export * from './markup/utils' diff --git a/foundations/core/packages/text-core/src/markup/__tests__/traverse.test.ts b/foundations/core/packages/text-core/src/markup/__tests__/traverse.test.ts new file mode 100644 index 0000000000..cafd4506dd --- /dev/null +++ b/foundations/core/packages/text-core/src/markup/__tests__/traverse.test.ts @@ -0,0 +1,97 @@ +import { MarkupNode, MarkupNodeType } from '../model' +import { traverseAllMarks, traverseNode, traverseNodeMarks } from '../traverse' + +describe('traverseNode', () => { + it('should call the callback function for each node', () => { + const callback = jest.fn() + const node = { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Hello, world!' + } + ] + } + + traverseNode(node as MarkupNode, callback) + + expect(callback).toHaveBeenCalledTimes(2) + expect(callback).toHaveBeenCalledWith(node, undefined) + expect(callback).toHaveBeenCalledWith(node.content[0], node) + }) + + it('should stop traversing if the callback returns false', () => { + const callback = jest.fn((node) => { + if (node.type === MarkupNodeType.paragraph) { + return false + } + }) + const node = { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'Hello, world!' + } + ] + } + + traverseNode(node, callback) + + expect(callback).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledWith(node, undefined) + }) +}) + +describe('traverseNodeMarks', () => { + it('should call the callback function for each mark', () => { + const callback = jest.fn() + const node = { + type: 'paragraph', + marks: [{ type: 'bold' }, { type: 'italic' }, { type: 'underline' }] + } + + traverseNodeMarks(node as MarkupNode, callback) + + expect(callback).toHaveBeenCalledTimes(3) + expect(callback).toHaveBeenCalledWith(node.marks[0]) + expect(callback).toHaveBeenCalledWith(node.marks[1]) + expect(callback).toHaveBeenCalledWith(node.marks[2]) + }) + + it('should not call the callback function if marks are not present', () => { + const callback = jest.fn() + const node = { + type: MarkupNodeType.paragraph + } + + traverseNodeMarks(node, callback) + + expect(callback).not.toHaveBeenCalled() + }) +}) + +describe('traverseAllMarks', () => { + it('should traverse all marks and call the callback function', () => { + const callback = jest.fn() + const node = { + type: 'paragraph', + marks: [{ type: 'bold' }], + content: [ + { + type: MarkupNodeType.text, + text: 'Hello, world!', + marks: [{ type: 'italic' }, { type: 'underline' }] + } + ] + } + + traverseAllMarks(node as MarkupNode, callback) + + expect(callback).toHaveBeenCalledTimes(3) + expect(callback).toHaveBeenCalledWith(node, node.marks[0]) + expect(callback).toHaveBeenCalledWith(node.content[0], node.content[0].marks[0]) + expect(callback).toHaveBeenCalledWith(node.content[0], node.content[0].marks[1]) + }) +}) diff --git a/foundations/core/packages/text-core/src/markup/__tests__/utils.test.ts b/foundations/core/packages/text-core/src/markup/__tests__/utils.test.ts new file mode 100644 index 0000000000..740aa1c64f --- /dev/null +++ b/foundations/core/packages/text-core/src/markup/__tests__/utils.test.ts @@ -0,0 +1,49 @@ +import { hashAttrs, stripHash } from '../utils' + +describe('hashAttrs', () => { + it('should return a hash of length 8', () => { + const attrs = { a: 1 } + const hash = hashAttrs(attrs) + expect(hash.length).toEqual(8) + }) + it('should return the same hash for the same attrs', () => { + const attrs = { a: 1, b: 2 } + const hash1 = hashAttrs(attrs) + const hash2 = hashAttrs(attrs) + expect(hash1).toEqual(hash2) + }) + + it('should return different hashes for different attrs', () => { + const attrs1 = { a: 1, b: 2 } + const attrs2 = { a: 1, b: 3 } + const hash1 = hashAttrs(attrs1) + const hash2 = hashAttrs(attrs2) + expect(hash1).not.toEqual(hash2) + }) +}) + +describe('stripHash', () => { + it('should return the name without the hash', () => { + const name = 'bold--c0decafe' + const result = stripHash(name) + expect(result).toEqual('bold') + }) + + it('should return the original name if no hash is present', () => { + const name = 'bold' + const result = stripHash(name) + expect(result).toEqual(name) + }) + + it('should return the original name if the hash is not 8 characters long', () => { + const name = 'bold--1234567' + const result = stripHash(name) + expect(result).toEqual(name) + }) + + it('should return the original name if the hash is not a valid base64 string', () => { + const name = 'bold--invalid!' + const result = stripHash(name) + expect(result).toEqual(name) + }) +}) diff --git a/foundations/core/packages/text-core/src/markup/dsl.ts b/foundations/core/packages/text-core/src/markup/dsl.ts new file mode 100644 index 0000000000..f72a1bb2db --- /dev/null +++ b/foundations/core/packages/text-core/src/markup/dsl.ts @@ -0,0 +1,81 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MarkupMark, MarkupMarkType, MarkupNode, MarkupNodeType } from './model' + +// Nodes + +export function nodeDoc (...content: MarkupNode[]): MarkupNode { + return node(MarkupNodeType.doc, ...content) +} + +export function nodeParagraph (...content: MarkupNode[]): MarkupNode { + return node(MarkupNodeType.paragraph, ...content) +} + +export function nodeText (text: string): MarkupNode { + return { type: MarkupNodeType.text, text } +} + +export function nodeImage (attrs: { src: string, alt?: string, width?: number, height?: number }): MarkupNode { + return { type: MarkupNodeType.image, attrs } +} + +export function nodeReference (attrs: { id: string, label: string, objectclass: string }): MarkupNode { + return { type: MarkupNodeType.reference, attrs } +} + +// Marks + +export function markBold (node: MarkupNode): MarkupNode { + return withMarks(node, mark(MarkupMarkType.bold)) +} + +export function markCode (node: MarkupNode): MarkupNode { + return withMarks(node, mark(MarkupMarkType.code)) +} + +export function markItalic (node: MarkupNode): MarkupNode { + return withMarks(node, mark(MarkupMarkType.em)) +} + +export function markStrike (node: MarkupNode): MarkupNode { + return withMarks(node, mark(MarkupMarkType.strike)) +} + +export function markUnderline (node: MarkupNode): MarkupNode { + return withMarks(node, mark(MarkupMarkType.underline)) +} + +export function markLink (attrs: { href: string, title: string }, node: MarkupNode): MarkupNode { + return withMarks(node, mark(MarkupMarkType.link, attrs)) +} + +// Utility + +function node (type: MarkupNodeType, ...content: MarkupNode[]): MarkupNode { + return { type, content } +} + +function mark (type: MarkupMarkType, attrs?: Record): MarkupMark { + return { type, attrs: attrs ?? {} } +} + +function withMarks (node: MarkupNode, ...marks: MarkupMark[]): MarkupNode { + const current = node.marks ?? [] + current.push(...marks) + + return { ...node, marks: current } +} diff --git a/foundations/core/packages/text-core/src/markup/model.ts b/foundations/core/packages/text-core/src/markup/model.ts new file mode 100644 index 0000000000..f061ca84cd --- /dev/null +++ b/foundations/core/packages/text-core/src/markup/model.ts @@ -0,0 +1,96 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/** @public */ +export enum MarkupNodeType { + doc = 'doc', + paragraph = 'paragraph', + blockquote = 'blockquote', + horizontal_rule = 'horizontalRule', + heading = 'heading', + code_block = 'codeBlock', + text = 'text', + image = 'image', + file = 'file', + reference = 'reference', + emoji = 'emoji', + hard_break = 'hardBreak', + ordered_list = 'orderedList', + bullet_list = 'bulletList', + list_item = 'listItem', + taskList = 'taskList', + taskItem = 'taskItem', + todoList = 'todoList', + todoItem = 'todoItem', + subLink = 'subLink', + table = 'table', + table_row = 'tableRow', + table_cell = 'tableCell', + table_header = 'tableHeader', + mermaid = 'mermaid', + comment = 'comment', + markdown = 'markdown', + embed = 'embed' +} + +/** @public */ +export enum MarkupMarkType { + link = 'link', + em = 'italic', + bold = 'bold', + code = 'code', + strike = 'strike', + underline = 'underline', + textColor = 'textColor', + textStyle = 'textStyle' +} + +/** @public */ +export interface MarkupMark { + type: MarkupMarkType + attrs?: Record // A map of attributes +} + +export type AttrValue = string | number | boolean | null | undefined +export type Attrs = Record + +/** @public */ +export interface MarkupNode { + type: MarkupNodeType + content?: MarkupNode[] // A list of child nodes + marks?: MarkupMark[] + attrs?: Attrs + text?: string +} + +/** @public */ +export function emptyMarkupNode (): MarkupNode { + return { + type: MarkupNodeType.doc, + content: [{ type: MarkupNodeType.paragraph, content: [] }] + } +} + +/** @public */ +export interface LinkMark extends MarkupMark { + href: string + title: string +} + +/** @public */ +export interface ReferenceMarkupNode extends MarkupNode { + type: MarkupNodeType.reference + attrs: { id: string, label: string, objectclass: string } +} diff --git a/foundations/core/packages/text-core/src/markup/reference.ts b/foundations/core/packages/text-core/src/markup/reference.ts new file mode 100644 index 0000000000..a9e4c64261 --- /dev/null +++ b/foundations/core/packages/text-core/src/markup/reference.ts @@ -0,0 +1,49 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Class, Doc, Ref } from '@hcengineering/core' +import { MarkupNode, MarkupNodeType, ReferenceMarkupNode } from '../markup/model' +import { traverseNode } from '../markup/traverse' + +/** + * @public + */ +export interface Reference { + objectId: Ref + objectClass: Ref> + parentNode: MarkupNode | null +} + +/** + * @public + */ +export function extractReferences (content: MarkupNode): Array { + const result: Array = [] + + traverseNode(content, (node, parent) => { + if (node.type === MarkupNodeType.reference) { + const reference = node as ReferenceMarkupNode + const objectId = reference.attrs.id as Ref + const objectClass = reference.attrs.objectclass as Ref> + const e = result.find((e) => e.objectId === objectId && e.objectClass === objectClass) + if (e === undefined) { + result.push({ objectId, objectClass, parentNode: parent ?? node }) + } + } + return true + }) + + return result +} diff --git a/foundations/core/packages/text-core/src/markup/traverse.ts b/foundations/core/packages/text-core/src/markup/traverse.ts new file mode 100644 index 0000000000..08f6dc1e5e --- /dev/null +++ b/foundations/core/packages/text-core/src/markup/traverse.ts @@ -0,0 +1,57 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MarkupMark, MarkupNode } from './model' + +export function traverseNode ( + node: MarkupNode, + fn: (el: MarkupNode, parent: MarkupNode | undefined) => boolean | undefined +): void { + _traverseNode(node, undefined, fn) +} + +function _traverseNode ( + node: MarkupNode, + parent: MarkupNode | undefined, + fn: (el: MarkupNode, parent: MarkupNode | undefined) => boolean | undefined +): void { + const result = fn(node, parent) + if (result !== false) { + node.content?.forEach((p) => { + _traverseNode(p, node, fn) + }) + } +} + +export function traverseNodeMarks (node: MarkupNode, f: (el: MarkupMark) => void): void { + node.marks?.forEach((p) => { + f(p) + }) +} + +export function traverseNodeContent (node: MarkupNode, f: (el: MarkupNode) => void): void { + node.content?.forEach((p) => { + f(p) + }) +} + +export function traverseAllMarks (node: MarkupNode, f: (el: MarkupNode, mark: MarkupMark) => void): void { + traverseNode(node, (node) => { + traverseNodeMarks(node, (mark) => { + f(node, mark) + }) + return true + }) +} diff --git a/foundations/core/packages/text-core/src/markup/utils.ts b/foundations/core/packages/text-core/src/markup/utils.ts new file mode 100644 index 0000000000..dd5efd7630 --- /dev/null +++ b/foundations/core/packages/text-core/src/markup/utils.ts @@ -0,0 +1,229 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Markup } from '@hcengineering/core' + +import { deepEqual } from 'fast-equals' +import hashIt from 'hash-it' + +import { nodeDoc, nodeParagraph, nodeText } from './dsl' +import { MarkupMark, MarkupMarkType, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model' +import { traverseAllMarks, traverseNode } from './traverse' + +/** @public */ +export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode()) + +/** @public */ +export function isEmptyMarkup (markup: Markup | undefined): boolean { + if (markup === undefined || markup === null || markup === '') { + return true + } + return isEmptyNode(markupToJSON(markup)) +} + +/** @public */ +export function areEqualMarkups (markup1: Markup, markup2: Markup): boolean { + if (markup1 === markup2) { + return true + } + + const node1 = markupToJSON(markup1) + const node2 = markupToJSON(markup2) + + if (isEmptyNode(node1) && isEmptyNode(node2)) { + return true + } + + return equalNodes(node1, node2) +} + +/** @public */ +export function areEqualJson (json1: MarkupNode, json2: MarkupNode): boolean { + return equalNodes(json1, json2) +} + +function equalNodes (node1: MarkupNode, node2: MarkupNode): boolean { + if (node1.type !== node2.type) return false + + const text1 = node1.text ?? '' + const text2 = node2.text ?? '' + if (text1 !== text2) return false + + if (!equalArrays(node1.content, node2.content, equalNodes)) return false + if (!equalArrays(node1.marks, node2.marks, equalMarks)) return false + if (!equalRecords(node1.attrs, node2.attrs)) return false + + return true +} + +function equalArrays (a: T[] | undefined, b: T[] | undefined, equal: (a: T, b: T) => boolean): boolean { + if (a === b) return true + const arr1 = a ?? [] + const arr2 = b ?? [] + if (arr1.length !== arr2.length) return false + return arr1.every((item1, i) => equal(item1, arr2[i])) +} + +function equalRecords (a: Record | undefined, b: Record | undefined): boolean { + if (a === b) return true + a = Object.fromEntries(Object.entries(a ?? {}).filter(([_, v]) => v != null)) + b = Object.fromEntries(Object.entries(b ?? {}).filter(([_, v]) => v != null)) + return deepEqual(a, b) +} + +export function equalMarks (a: MarkupMark, b: MarkupMark): boolean { + return a.type === b.type && equalRecords(a.attrs, b.attrs) +} + +const emptyNodes = [MarkupNodeType.hard_break] + +const nonEmptyNodes = [ + MarkupNodeType.horizontal_rule, + MarkupNodeType.image, + MarkupNodeType.reference, + MarkupNodeType.emoji, + MarkupNodeType.subLink, + MarkupNodeType.table +] + +/** @public */ +export function isEmptyNode (node: MarkupNode): boolean { + if (emptyNodes.includes(node.type)) return true + if (nonEmptyNodes.includes(node.type)) return false + if (node.text !== undefined && node.text?.trim().length > 0) return false + + const content = node.content ?? [] + return content.every(isEmptyNode) +} + +// Markup + +/** @public */ +export function jsonToMarkup (json: MarkupNode): Markup { + return JSON.stringify(json) +} + +/** @public */ +export function markupToJSON (markup: Markup): MarkupNode { + if (markup == null || markup === '') { + return emptyMarkupNode() + } + + try { + // Ideally Markup should contain only serialized JSON + // But there seem to be some cases when it contains HTML or plain text + // So we need to handle those cases and produce valid MarkupNode + if (markup.startsWith('{')) { + const json = JSON.parse(markup) as MarkupNode + traverseAllMarks(json, (node, mark) => { + mark.type = stripHash(mark.type) as MarkupMarkType + }) + return json + } else { + return nodeDoc(nodeParagraph(nodeText(markup))) + } + } catch (error) { + return emptyMarkupNode() + } +} + +// UTILS + +const ELLIPSIS_CHAR = '…' +const WHITESPACE = ' ' + +/** @public */ +export function stripTags (markup: Markup, textLimit = 0): string { + const parsed = markupToJSON(markup) + + const textParts: string[] = [] + let charCount = 0 + let isHardStop = false + + const pushText = (text: string): void => { + if (textLimit > 0 && charCount + text.length > textLimit) { + const toAddCount = textLimit - charCount + const textPart = text.substring(0, toAddCount) + textParts.push(textPart) + textParts.push(ELLIPSIS_CHAR) + isHardStop = true + } else { + textParts.push(text) + charCount += text.length + } + } + + traverseNode(parsed, (node, parent): boolean => { + if (isHardStop) { + return false + } + + if (node.type === MarkupNodeType.text) { + const text = node.text ?? '' + pushText(text) + return false + } else if ( + node.type === MarkupNodeType.paragraph || + node.type === MarkupNodeType.table || + node.type === MarkupNodeType.doc || + node.type === MarkupNodeType.blockquote + ) { + if (textParts.length > 0 && textParts[textParts.length - 1] !== WHITESPACE) { + textParts.push(WHITESPACE) + charCount++ + } + } else if (node.type === MarkupNodeType.reference) { + const label = `${node.attrs?.label ?? ''}` + pushText(label.length > 0 ? `@${label}` : '') + } + return true + }) + + const result = textParts.join('') + return result +} + +export function markupToText (markup: Markup): string { + const jsonModel = markupToJSON(markup) + const fragments: string[] = [] + + traverseNode(jsonModel, (node) => { + if (node.type === MarkupNodeType.text) { + const text = node.text ?? '' + if (node.text !== undefined && node.text.length > 0) { + fragments.push(text) + } + } else if (node.type === MarkupNodeType.paragraph) { + fragments.push('\n\n') + } + return true + }) + + return fragments.join('').trim() +} + +// see https://github.com/yjs/y-prosemirror/blob/master/src/lib.js#L402 +const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/ + +/** @public */ +export function stripHash (attrName: string): string { + return hashedMarkNameRegex.exec(attrName)?.[1] ?? attrName +} + +/** @public */ +export function hashAttrs (attrs: any): string { + const hash = hashIt(attrs) + return (hash >>> 0).toString(16).padStart(8, '0') +} diff --git a/foundations/core/packages/text-core/tsconfig.json b/foundations/core/packages/text-core/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/text-core/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/text-html/.eslintrc.js b/foundations/core/packages/text-html/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/text-html/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/text-html/CHANGELOG.json b/foundations/core/packages/text-html/CHANGELOG.json new file mode 100644 index 0000000000..f3e5322660 --- /dev/null +++ b/foundations/core/packages/text-html/CHANGELOG.json @@ -0,0 +1,56 @@ +{ + "name": "@hcengineering/text-html", + "entries": [ + { + "version": "0.7.18", + "tag": "@hcengineering/text-html_v0.7.18", + "date": "Mon, 27 Oct 2025 16:46:51 GMT", + "comments": { + "patch": [ + { + "comment": "add support for textColor and textStyle marks" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/text-html_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/text-html_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/text-html/CHANGELOG.md b/foundations/core/packages/text-html/CHANGELOG.md new file mode 100644 index 0000000000..e1465e07a7 --- /dev/null +++ b/foundations/core/packages/text-html/CHANGELOG.md @@ -0,0 +1,25 @@ +# Change Log - @hcengineering/text-html + +This log was last generated on Mon, 27 Oct 2025 16:46:51 GMT and should not be manually modified. + +## 0.7.18 +Mon, 27 Oct 2025 16:46:51 GMT + +### Patches + +- add support for textColor and textStyle marks + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + diff --git a/foundations/core/packages/text-html/config/rig.json b/foundations/core/packages/text-html/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/text-html/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/text-html/jest.config.js b/foundations/core/packages/text-html/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/text-html/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/text-html/package.json b/foundations/core/packages/text-html/package.json new file mode 100644 index 0000000000..a0921a2bb4 --- /dev/null +++ b/foundations/core/packages/text-html/package.json @@ -0,0 +1,57 @@ +{ + "name": "@hcengineering/text-html", + "version": "0.7.18", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "build:watch": "compile", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/text-core": "workspace:^0.7.18", + "htmlparser2": "^9.0.0" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/text-html/src/__tests__/html.test.ts b/foundations/core/packages/text-html/src/__tests__/html.test.ts new file mode 100644 index 0000000000..7df7fb1c60 --- /dev/null +++ b/foundations/core/packages/text-html/src/__tests__/html.test.ts @@ -0,0 +1,581 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MarkupNode } from '@hcengineering/text-core' +import { htmlToMarkup, markupToHtml } from '..' + +const tests: Array<{ name: string, markup: object, html: string }> = [ + { + name: 'paragraph', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'paragraph 1' + } + ] + } + ] + }, + html: '

paragraph 1

' + }, + { + name: 'text alignment', + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { + level: 1, + textAlign: 'left' + }, + content: [ + { + type: 'text', + text: 'heading 1' + } + ] + }, + { + type: 'paragraph', + attrs: { + textAlign: 'right' + }, + content: [ + { + type: 'text', + text: 'paragraph 1' + } + ] + } + ] + }, + html: '

heading 1

paragraph 1

' + }, + { + name: 'headings', + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { + level: 1, + textAlign: null + }, + content: [ + { + type: 'text', + text: 'heading 1' + } + ] + }, + { + type: 'heading', + attrs: { + level: 2, + textAlign: null + }, + content: [ + { + type: 'text', + text: 'heading 2' + } + ] + }, + { + type: 'heading', + attrs: { + level: 3, + textAlign: null + }, + content: [ + { + type: 'text', + text: 'heading 3' + } + ] + }, + { + type: 'heading', + attrs: { + level: 4, + textAlign: null + }, + content: [ + { + type: 'text', + text: 'heading 4' + } + ] + }, + { + type: 'heading', + attrs: { + level: 5, + textAlign: null + }, + content: [ + { + type: 'text', + text: 'heading 5' + } + ] + }, + { + type: 'heading', + attrs: { + level: 6, + textAlign: null + }, + content: [ + { + type: 'text', + text: 'heading 6' + } + ] + } + ] + }, + html: '

heading 1

heading 2

heading 3

heading 4

heading 5
heading 6
' + }, + { + name: 'blockquote', + markup: { + type: 'doc', + content: [ + { + type: 'blockquote', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'Lorem ipsum dolor sit amet.' + } + ] + } + ] + } + ] + }, + html: '

Lorem ipsum dolor sit amet.

' + }, + { + name: 'codeblock', + markup: { + type: 'doc', + content: [ + { + type: 'codeBlock', + attrs: { + language: 'typescript' + }, + content: [ + { + type: 'text', + text: 'const x: number = 42;' + } + ] + } + ] + }, + html: '
const x: number = 42;
' + }, + { + name: 'hr', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'paragraph 1' + } + ] + }, + { + type: 'horizontalRule' + }, + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'paragraph 2' + } + ] + } + ] + }, + html: '

paragraph 1


paragraph 2

' + }, + { + name: 'br', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'line 1' + }, + { + type: 'hardBreak' + }, + { + type: 'text', + text: 'line 2' + } + ] + } + ] + }, + html: '

line 1
line 2

' + }, + { + name: 'ordered list', + markup: { + type: 'doc', + content: [ + { + type: 'orderedList', + attrs: { + start: 1 + }, + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'item 1' + } + ] + } + ] + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'item 2' + } + ] + } + ] + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'item 3' + } + ] + } + ] + } + ] + } + ] + }, + html: '
  1. item 1

  2. item 2

  3. item 3

' + }, + { + name: 'bullet list', + markup: { + type: 'doc', + content: [ + { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'item 1' + } + ] + } + ] + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'item 2' + } + ] + } + ] + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'item 3' + } + ] + } + ] + } + ] + } + ] + }, + html: '
  • item 1

  • item 2

  • item 3

' + }, + { + name: 'ref', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'hello ' + }, + { + type: 'reference', + attrs: { + id: '64708c79c8f2613474dea38b', + objectclass: 'contact:class:Person', + label: 'John Doe' + } + } + ] + } + ] + }, + html: '

hello @John Doe

' + }, + { + name: 'embed', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'hello ' + }, + { + type: 'embed', + attrs: { + src: 'http://localhost/embed' + } + } + ] + } + ] + }, + html: '

hello http://localhost/embed

' + }, + { + name: 'embed-uri-escape', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'hello ' + }, + { + type: 'embed', + attrs: { + src: 'http://localhost/embed spaces' + } + } + ] + } + ] + }, + html: '

hello http://localhost/embed spaces

' + }, + { + name: 'embed-html-escape', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'hello ' + }, + { + type: 'embed', + attrs: { + src: 'http://localhost/embed' + } + } + ] + } + ] + }, + html: '

hello http://localhost/embed<html>

' + }, + { + name: 'link-with-class', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: 'https://example.com', + marks: [ + { + type: 'link', + attrs: { + href: 'https://example.com', + target: '_blank', + rel: 'noopener noreferrer', + class: 'cursor-pointer', + title: undefined + } + } + ] + } + ] + } + ] + }, + html: '

https://example.com

' + } +] + +describe('markupToHtml', () => { + describe('convert to html', () => { + tests.forEach(({ name, markup, html }) => { + it(name, () => { + const result = markupToHtml(markup as MarkupNode) + expect(result).toEqual(html) + }) + }) + }) +}) + +describe('htmlToMarkup', () => { + describe('convert to markup', () => { + tests.forEach(({ name, markup, html }) => { + it(name, () => { + const result = htmlToMarkup(html) + expect(result).toEqual(markup) + }) + }) + }) + + describe('convert to markup and back', () => { + tests.forEach(({ name, markup, html }) => { + it(name, () => { + const result = htmlToMarkup(html) + const serialized = markupToHtml(result) + expect(serialized).toEqual(html) + }) + }) + }) +}) diff --git a/foundations/core/packages/text-html/src/index.ts b/foundations/core/packages/text-html/src/index.ts new file mode 100644 index 0000000000..1462f0008a --- /dev/null +++ b/foundations/core/packages/text-html/src/index.ts @@ -0,0 +1,28 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type MarkupNode } from '@hcengineering/text-core' +import { type HtmlParserOptions, HtmlParser } from './parser' +import { type HtmlSerializerOptions, HtmlSerializer } from './serializer' + +export function markupToHtml (markup: MarkupNode, options: HtmlSerializerOptions = {}): string { + const serializer = new HtmlSerializer(options) + return serializer.serialize(markup) +} + +export function htmlToMarkup (html: string, options: HtmlParserOptions = {}): MarkupNode { + const parser = new HtmlParser(options) + return parser.parse(html) +} diff --git a/foundations/core/packages/text-html/src/parser.ts b/foundations/core/packages/text-html/src/parser.ts new file mode 100644 index 0000000000..44a8aa9fe9 --- /dev/null +++ b/foundations/core/packages/text-html/src/parser.ts @@ -0,0 +1,626 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type AttrValue, + type MarkupMark, + type MarkupNode, + MarkupMarkType, + MarkupNodeType +} from '@hcengineering/text-core' +import { Parser } from 'htmlparser2' + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface HtmlParserOptions {} + +interface HtmlTagHandler { + handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => void + handleCloseTag: (state: HtmlParseState, tag: string) => void +} + +interface HtmlNodeRule { + node: MarkupNodeType + getAttrs?: Record | ((attrs: Record) => Record | undefined) + wrapNode?: boolean + wrapContent?: boolean +} + +interface HtmlMarkRule { + mark: MarkupMarkType + getAttrs?: Record | ((attrs: Record) => Record | undefined) +} + +interface HtmlSpecialRule { + handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => void + handleCloseTag: (state: HtmlParseState, tag: string) => void +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface HtmlIgnoreRule {} + +interface HtmlStyleRule { + style: string + getAttrs?: (value: string) => Record | undefined +} + +class HtmlParseState { + private readonly stack: MarkupNode[] = [] + private readonly marks: MarkupMark[] = [] + + constructor ( + readonly root: MarkupNode, + readonly handlers: Record + ) { + this.stack.push(root) + } + + top (): MarkupNode | undefined { + return this.stack[this.stack.length - 1] + } + + addText (text: string): void { + const top = this.top() + if (top === undefined || text.length === 0) { + return + } + + // Auto-wrap text in paragraph if we're at the doc level + if (top.type === MarkupNodeType.doc) { + this.openNode(MarkupNodeType.paragraph) + const node: MarkupNode = + this.marks.length > 0 + ? { type: MarkupNodeType.text, text, marks: [...this.marks] } + : { type: MarkupNodeType.text, text } + this.push(node) + this.closeNode(MarkupNodeType.paragraph) + return + } + + const node: MarkupNode = + this.marks.length > 0 + ? { type: MarkupNodeType.text, text, marks: [...this.marks] } + : { type: MarkupNodeType.text, text } + + this.push(node) + } + + openMark (mark: MarkupMarkType, attrs?: Record): void { + this.marks.push(attrs !== undefined ? { type: mark, attrs } : { type: mark }) + } + + closeMark (mark: MarkupMarkType): void { + if (this.marks[this.marks.length - 1]?.type === mark) { + this.marks.pop() + } + } + + openNode (node: MarkupNodeType, attrs?: Record): void { + this.stack.push(attrs !== undefined ? { type: node, attrs } : { type: node }) + } + + closeNode (node: MarkupNodeType): void { + this.marks.splice(0) + const info = this.stack.pop() + if (info !== undefined) { + this.push(info) + } + } + + addNode (node: MarkupNode): void { + this.push(node) + } + + push (node: MarkupNode): void { + const parent = this.top() + if (parent !== undefined) { + const content = parent.content ?? [] + content.push(node) + parent.content = content + } + } +} + +function nodeHandler ({ node, getAttrs, wrapContent, wrapNode }: HtmlNodeRule): HtmlTagHandler { + const wrapStack: boolean[] = [] + + return { + handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { + const attrs = + typeof getAttrs === 'function' + ? getAttrs(attributes) + : typeof getAttrs === 'object' + ? { ...getAttrs } + : undefined + + const shouldWrapNode = wrapNode === true && state.top()?.type !== MarkupNodeType.paragraph + + if (shouldWrapNode) { + state.openNode(MarkupNodeType.paragraph) + } + wrapStack.push(shouldWrapNode) + + state.openNode(node, attrs) + + if (wrapContent === true) { + state.openNode(MarkupNodeType.paragraph) + } + }, + handleCloseTag: (state: HtmlParseState, tag: string) => { + if (wrapContent === true && state.top()?.type === MarkupNodeType.paragraph) { + state.closeNode(MarkupNodeType.paragraph) + } + + state.closeNode(node) + if (wrapStack.pop() === true) { + state.closeNode(MarkupNodeType.paragraph) + } + } + } +} + +function markHandler ({ mark, getAttrs }: HtmlMarkRule): HtmlTagHandler { + return { + handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { + const attrs = + typeof getAttrs === 'function' + ? getAttrs(attributes) + : typeof getAttrs === 'object' + ? { ...getAttrs } + : undefined + state.openMark(mark, attrs) + }, + handleCloseTag: (state: HtmlParseState, tag: string) => { + state.closeMark(mark) + } + } +} + +function ignoreHandler (rule: HtmlIgnoreRule): HtmlTagHandler { + return { + handleOpenTag: () => {}, + handleCloseTag: () => {} + } +} + +function specialHandler (rule: HtmlSpecialRule): HtmlTagHandler { + return { + handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { + rule.handleOpenTag(state, tag, attributes) + }, + handleCloseTag: (state: HtmlParseState, tag: string) => { + rule.handleCloseTag(state, tag) + } + } +} + +const styleRules: HtmlStyleRule[] = [ + { + style: 'text-align', + getAttrs: (value: string) => { + return { textAlign: value ?? null } + } + } +] + +const markRules: Record = { + b: { + mark: MarkupMarkType.bold + }, + em: { + mark: MarkupMarkType.em + }, + i: { + mark: MarkupMarkType.em + }, + s: { + mark: MarkupMarkType.strike + }, + strong: { + mark: MarkupMarkType.bold + }, + u: { + mark: MarkupMarkType.underline + } +} + +const nodeRules: Record = { + h1: { + node: MarkupNodeType.heading, + getAttrs: (attributes: Record) => { + return { + level: 1, + textAlign: attributes.textAlign ?? null + } + } + }, + h2: { + node: MarkupNodeType.heading, + getAttrs: (attributes: Record) => { + return { + level: 2, + textAlign: attributes.textAlign ?? null + } + } + }, + h3: { + node: MarkupNodeType.heading, + getAttrs: (attributes: Record) => { + return { + level: 3, + textAlign: attributes.textAlign ?? null + } + } + }, + h4: { + node: MarkupNodeType.heading, + getAttrs: (attributes: Record) => { + return { + level: 4, + textAlign: attributes.textAlign ?? null + } + } + }, + h5: { + node: MarkupNodeType.heading, + getAttrs: (attributes: Record) => { + return { + level: 5, + textAlign: attributes.textAlign ?? null + } + } + }, + h6: { + node: MarkupNodeType.heading, + getAttrs: (attributes: Record) => { + return { + level: 6, + textAlign: attributes.textAlign ?? null + } + } + }, + blockquote: { + node: MarkupNodeType.blockquote + }, + pre: { + node: MarkupNodeType.code_block + }, + hr: { + node: MarkupNodeType.horizontal_rule + }, + br: { + node: MarkupNodeType.hard_break + }, + ol: { + node: MarkupNodeType.ordered_list, + getAttrs: (attributes: Record) => { + return { + start: parseInt(attributes.starts ?? '1') + } + } + }, + ul: { + node: MarkupNodeType.bullet_list + }, + li: { + node: MarkupNodeType.list_item + }, + img: { + node: MarkupNodeType.image, + wrapNode: true, + getAttrs: (attributes: Record) => { + return { + src: attributes.src, + alt: attributes.alt, + title: attributes.title ?? null, + width: attributes.width !== undefined ? parseInt(attributes.width) : undefined, + height: attributes.height !== undefined ? parseInt(attributes.height) : undefined, + 'file-id': attributes['file-id'] ?? null + } + } + }, + sub: { + node: MarkupNodeType.subLink + }, + table: { + node: MarkupNodeType.table + }, + tr: { + node: MarkupNodeType.table_row + }, + th: { + node: MarkupNodeType.table_header, + getAttrs: (attributes: Record) => { + return { + colspan: attributes.colspan !== undefined ? parseInt(attributes.colspan) : undefined, + rowspan: attributes.rowspan !== undefined ? parseInt(attributes.rowspan) : undefined, + colwidth: attributes.colwidth !== undefined ? parseInt(attributes.colwidth) : undefined + } + }, + wrapContent: true + }, + td: { + node: MarkupNodeType.table_cell, + getAttrs: (attributes: Record) => { + return { + colspan: attributes.colspan !== undefined ? parseInt(attributes.colspan) : undefined, + rowspan: attributes.rowspan !== undefined ? parseInt(attributes.rowspan) : undefined, + colwidth: attributes.colwidth !== undefined ? parseInt(attributes.colwidth) : undefined + } + }, + wrapContent: true + }, + comment: { + node: MarkupNodeType.comment + } +} + +const specialRules: Record = { + p: { + handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { + const top = state.top() + if (top?.type !== MarkupNodeType.paragraph) { + const attrs = { textAlign: attributes.textAlign ?? null } + state.openNode(MarkupNodeType.paragraph, attrs) + } + }, + handleCloseTag: (state: HtmlParseState, tag: string) => { + const top = state.top() + if (top?.type === MarkupNodeType.paragraph) { + state.closeNode(MarkupNodeType.paragraph) + } + } + }, + span: { + handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { + const dataType = attributes['data-type'] + const dataColor = attributes['data-color'] + const style = attributes.style + + if (dataType === 'reference') { + state.openNode(MarkupNodeType.reference, { + id: attributes['data-id'], + objectclass: attributes['data-objectclass'], + label: attributes['data-label'] + }) + } else if (dataColor !== undefined) { + state.openMark(MarkupMarkType.textColor, { + color: dataColor + }) + } else if (style !== undefined) { + const attrs: Record = {} + style.split(';').forEach((part) => { + const [key, value] = part.split(':') + if (key !== undefined && value !== undefined) { + const trimmedKey = key.trim() + const trimmedValue = value.trim() + if (trimmedKey.length > 0 && trimmedValue.length > 0) { + // Convert CSS property names to camelCase (e.g., font-family -> fontFamily) + const camelKey = trimmedKey.replace(/-([a-z])/g, (g) => g[1].toUpperCase()) + attrs[camelKey] = trimmedValue + } + } + }) + + if (Object.keys(attrs).length > 0) { + state.openMark(MarkupMarkType.textStyle, attrs) + } + } + }, + handleCloseTag: (state: HtmlParseState, tag: string) => { + const top = state.top() + if (top?.type === MarkupNodeType.reference) { + delete top.content + state.closeNode(MarkupNodeType.reference) + } else { + state.closeMark(MarkupMarkType.textColor) + state.closeMark(MarkupMarkType.textStyle) + } + } + }, + code: { + handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { + const top = state.top() + if (top?.type === MarkupNodeType.code_block) { + const classes = attributes.class?.split(' ') ?? [] + const language = classes.find((c) => c.startsWith('language-')) + if (language !== undefined) { + top.attrs = { language: language.substring(9) } + } + } else { + state.openMark(MarkupMarkType.code) + } + }, + handleCloseTag: (state: HtmlParseState, tag: string) => { + const top = state.top() + if (top?.type === MarkupNodeType.code_block) { + // do nothing + } else { + state.closeMark(MarkupMarkType.code) + } + } + }, + a: { + handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { + const dataType = attributes['data-type'] + if (dataType === 'embed') { + state.openNode(MarkupNodeType.embed, { + src: decodeURI(attributes.href) + }) + } else { + state.openMark(MarkupMarkType.link, { + rel: attributes.rel, + target: attributes.target, + class: attributes.class, + href: attributes.href, + title: attributes.title + }) + } + }, + handleCloseTag: (state: HtmlParseState, tag: string) => { + const top = state.top() + if (top?.type === MarkupNodeType.embed) { + delete top.content + state.closeNode(MarkupNodeType.embed) + } else { + state.closeMark(MarkupMarkType.link) + } + } + } +} + +const ignoreRules: Record = { + html: {}, + head: {}, + body: {}, + thead: {}, + tbody: {} +} + +export class HtmlParser { + private readonly handlers: Record = {} + + constructor (private readonly options: HtmlParserOptions = {}) { + Object.entries(nodeRules).forEach(([tag, rule]) => { + this.handlers[tag] = nodeHandler(rule) + }) + + Object.entries(markRules).forEach(([tag, rule]) => { + this.handlers[tag] = markHandler(rule) + }) + + Object.entries(specialRules).forEach(([tag, rule]) => { + this.handlers[tag] = specialHandler(rule) + }) + + Object.entries(ignoreRules).forEach(([tag, rule]) => { + this.handlers[tag] = ignoreHandler(rule) + }) + } + + parse (html: string): MarkupNode { + const root: MarkupNode = { type: MarkupNodeType.doc, content: [] } + const state = new HtmlParseState(root, this.handlers) + + let rawPos: number | undefined + let rawDepth: number | undefined + + const parser = new Parser( + { + onopentag (tag: string, attributes: Record) { + if (rawDepth !== undefined) { + rawDepth += 1 + return + } + + if (attributes.style !== undefined) { + const attrs = extractStyleAttrs(attributes, styleRules) + if (attrs !== undefined) { + attributes = { ...attributes, ...attrs } + } + } + + const handler = state.handlers[tag] + if (handler !== undefined) { + handler.handleOpenTag(state, tag, attributes) + } else { + rawPos = parser.startIndex + rawDepth = 0 + } + }, + + ontext (text: string) { + if (rawDepth !== undefined) { + return + } + + if (text.length === 0) { + return + } + + state.addText(text) + }, + + oncomment (text: string) { + if (rawDepth !== undefined) { + return + } + + state.openNode(MarkupNodeType.comment) + state.addText(text) + state.closeNode(MarkupNodeType.comment) + }, + + onclosetag (tag: string) { + if (rawDepth !== undefined) { + rawDepth -= 1 + + if (rawDepth === 0) { + const start = rawPos ?? 0 + const end = parser.endIndex + const text = html.substring(start, end) + state.addText(text) + rawPos = undefined + rawDepth = undefined + } + return + } + + const handler = state.handlers[tag] + if (handler !== undefined) { + handler.handleCloseTag(state, tag) + } else { + // do nothing + } + } + }, + { + decodeEntities: true, + lowerCaseTags: true, + recognizeSelfClosing: true + } + ) + + parser.write(html) + parser.end() + + return state.root + } +} + +function extractStyleAttrs (attrs: Record, rules: HtmlStyleRule[]): Record | undefined { + const style = attrs.style + if (style !== undefined) { + const styles: Record = {} + + // parse style attribute + style.split(';').forEach((stylePart) => { + const [key, value] = stylePart.split(':') + styles[key.trim()] = value?.trim() + }) + + const result = {} + + rules.forEach((rule) => { + if (styles[rule.style] !== undefined) { + const attrs = rule.getAttrs?.(styles[rule.style]) + if (attrs !== undefined) { + Object.assign(result, attrs) + } + } + }) + + return result + } +} diff --git a/foundations/core/packages/text-html/src/serializer.ts b/foundations/core/packages/text-html/src/serializer.ts new file mode 100644 index 0000000000..ff427a2e4c --- /dev/null +++ b/foundations/core/packages/text-html/src/serializer.ts @@ -0,0 +1,373 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + type Attrs, + type AttrValue, + type MarkupMark, + type MarkupNode, + MarkupMarkType, + MarkupNodeType +} from '@hcengineering/text-core' + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface HtmlSerializerOptions {} + +export class HtmlSerializer { + constructor (private readonly options: HtmlSerializerOptions = {}) {} + + serialize (markup: MarkupNode): string { + const builder = new NodeBuilder(true) + addNode(builder, markup) + return builder.toText() + } +} + +class NodeBuilder { + textParts: string[] = [] + + constructor (private readonly addTags: boolean) {} + + addText (text: string): void { + this.textParts.push(text) + } + + openTag ( + tag: string, + attributes: Record = {}, + options?: { newLine?: boolean, selfClosing?: boolean } + ): void { + if (this.addTags) { + this.textParts.push('<') + this.textParts.push(tag) + + for (const [key, value] of Object.entries(attributes)) { + if (value == null) continue + + if (typeof value === 'boolean') { + if (value) { + this.textParts.push(` ${key}`) + } + } else { + this.textParts.push(` ${key}="${escapeHtml(String(value))}"`) + } + } + + this.textParts.push(options?.selfClosing === true ? '/>' : '>') + } else { + if (options?.newLine === true) { + this.textParts.push('\n') + } + } + } + + closeTag (tag: string, options?: { newLine: boolean }): void { + if (this.addTags) { + this.textParts.push(``) + } else if (options?.newLine === true) { + this.textParts.push('\n') + } + } + + toText (): string { + return this.textParts.join('') + } +} + +// Helper function to escape HTML special characters +function escapeHtml (text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\r/g, ' ') + .replace(/\n/g, ' ') +} + +function addMark (builder: NodeBuilder, mark?: MarkupMark, next?: () => void): void { + if (mark != null) { + const attrs = mark.attrs ?? {} + + if (mark.type === MarkupMarkType.bold) { + builder.openTag('strong') + next?.() + builder.closeTag('strong') + } else if (mark.type === MarkupMarkType.code) { + builder.openTag('code') + next?.() + builder.closeTag('code') + } else if (mark.type === MarkupMarkType.em) { + builder.openTag('em') + next?.() + builder.closeTag('em') + } else if (mark.type === MarkupMarkType.link) { + builder.openTag('a', { + target: attrs.target, + rel: attrs.rel, + class: attrs.class, + href: attrs.href, + title: attrs.title + }) + next?.() + builder.closeTag('a') + } else if (mark.type === MarkupMarkType.strike) { + builder.openTag('s') + next?.() + builder.closeTag('s') + } else if (mark.type === MarkupMarkType.underline) { + builder.openTag('u') + next?.() + builder.closeTag('u') + } else if (mark.type === MarkupMarkType.textColor) { + const color = attrs.color + builder.openTag('span', { + style: color !== undefined ? `color: ${color}` : undefined, + 'data-color': color + }) + next?.() + builder.closeTag('span') + } else if (mark.type === MarkupMarkType.textStyle) { + // Convert camelCase properties back to kebab-case CSS properties + const styleAttrs = Object.entries(attrs) + .map(([key, value]) => { + const kebabKey = key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`) + return `${kebabKey}: ${value}` + }) + .join('; ') + + builder.openTag('span', { + style: styleAttrs + }) + next?.() + builder.closeTag('span') + } else { + // Handle unknown mark as span with data attribute + builder.openTag('span', { 'data-mark-type': mark.type as string }) + next?.() + builder.closeTag('span') + } + } +} + +function addMarks (builder: NodeBuilder, marks: MarkupMark[], next?: () => void): void { + if (marks.length > 0) { + const mark = marks[0] + const others = marks.slice(1) + + if (others.length > 0) { + addMark(builder, mark, () => { + addMarks(builder, others, next) + }) + } else { + addMark(builder, mark, next) + } + } +} + +function addNodes (builder: NodeBuilder, nodes: MarkupNode[]): void { + nodes.forEach((childNode) => { + addNode(builder, childNode) + }) +} + +function addNodeContent (builder: NodeBuilder, node?: MarkupNode): void { + if (node == null) return + + const attrs = node.attrs ?? {} + const nodes = node.content ?? [] + const style = toStyleAttr(attrs) + + if (node.type === MarkupNodeType.doc) { + addNodes(builder, nodes) + } else if (node.type === MarkupNodeType.paragraph) { + builder.openTag('p', { style }) + addNodes(builder, nodes) + builder.closeTag('p') + } else if (node.type === MarkupNodeType.blockquote) { + builder.openTag('blockquote') + addNodes(builder, nodes) + builder.closeTag('blockquote') + } else if (node.type === MarkupNodeType.horizontal_rule) { + builder.openTag('hr', {}, { selfClosing: true }) + } else if (node.type === MarkupNodeType.heading) { + const level = toNumber(attrs.level) ?? 1 + const tag = `h${level}` + builder.openTag(tag, { style }) + addNodes(builder, nodes) + builder.closeTag(tag) + } else if (node.type === MarkupNodeType.code_block) { + const attrs = node.attrs?.language !== undefined ? { class: `language-${node.attrs.language}` } : {} + builder.openTag('pre') + builder.openTag('code', attrs) + addNodes(builder, nodes) + builder.closeTag('code') + builder.closeTag('pre') + } else if (node.type === MarkupNodeType.text) { + builder.addText(node.text ?? '') + } else if (node.type === MarkupNodeType.image) { + const src = toString(attrs.src) + const alt = toString(attrs.alt) + const width = toString(attrs.width) + const height = toString(attrs.height) + builder.openTag('img', { src, alt, width, height }, { selfClosing: true }) + } else if (node.type === MarkupNodeType.reference) { + const label = toString(attrs.label) + builder.openTag('span', { + 'data-type': 'reference', + 'data-id': attrs.id, + 'data-objectclass': attrs.objectclass, + 'data-label': attrs.label + }) + builder.addText(label !== undefined ? `@${label}` : '') + builder.closeTag('span') + } else if (node.type === MarkupNodeType.hard_break) { + builder.openTag('br', {}, { selfClosing: true }) + } else if (node.type === MarkupNodeType.ordered_list) { + builder.openTag('ol') + addNodes(builder, nodes) + builder.closeTag('ol') + } else if (node.type === MarkupNodeType.bullet_list) { + builder.openTag('ul') + addNodes(builder, nodes) + builder.closeTag('ul') + } else if (node.type === MarkupNodeType.list_item) { + builder.openTag('li') + addNodes(builder, nodes) + builder.closeTag('li') + } else if (node.type === MarkupNodeType.todoList) { + builder.openTag('ul', { 'data-type': MarkupNodeType.todoList }) + addNodes(builder, nodes) + builder.closeTag('ul') + } else if (node.type === MarkupNodeType.todoItem) { + const checked = node.attrs?.checked === true || node.attrs?.checked === 'true' + const disabled = node.attrs?.disabled === true || node.attrs?.disabled === 'true' + + builder.openTag('li', { + 'data-type': MarkupNodeType.todoItem, + 'data-todoid': node.attrs?.todoid, + 'data-userid': node.attrs?.userid, + 'data-checked': checked + }) + + builder.openTag('input', { type: 'checkbox', checked, disabled }, { selfClosing: true }) + + addNodes(builder, nodes) + + builder.closeTag('li') + } else if (node.type === MarkupNodeType.taskList) { + builder.openTag('ul', { 'data-type': MarkupNodeType.taskList }) + addNodes(builder, nodes) + builder.closeTag('ul') + } else if (node.type === MarkupNodeType.taskItem) { + const checked = node.attrs?.checked === true || node.attrs?.checked === 'true' + const disabled = node.attrs?.disabled === true || node.attrs?.disabled === 'true' + + builder.openTag('li', { + 'data-type': MarkupNodeType.taskItem, + 'data-checked': checked + }) + builder.openTag('input', { type: 'checkbox', checked, disabled }, { selfClosing: true }) + + addNodes(builder, nodes) + + builder.closeTag('li') + } else if (node.type === MarkupNodeType.subLink) { + builder.openTag('sub') + addNodes(builder, nodes) + builder.closeTag('sub') + } else if (node.type === MarkupNodeType.table) { + builder.openTag('table') + builder.openTag('tbody') + addNodes(builder, nodes) + builder.closeTag('tbody') + builder.closeTag('table') + } else if (node.type === MarkupNodeType.table_row) { + builder.openTag('tr') + addNodes(builder, nodes) + builder.closeTag('tr') + } else if (node.type === MarkupNodeType.table_cell) { + const colspan = toNumber(attrs.colspan) ?? 1 + const rowspan = toNumber(attrs.rowspan) ?? 1 + const colwidth = toNumber(attrs.colwidth) + builder.openTag('td', { + colspan: colspan !== 1 ? colspan : undefined, + rowspan: rowspan !== 1 ? rowspan : undefined, + colwidth: colwidth !== undefined && colwidth > 0 ? colwidth : undefined + }) + addNodes(builder, nodes) + builder.closeTag('td') + } else if (node.type === MarkupNodeType.table_header) { + const colspan = toNumber(attrs.colspan) ?? 1 + const rowspan = toNumber(attrs.rowspan) ?? 1 + const colwidth = toNumber(attrs.colwidth) + builder.openTag('th', { + colspan: colspan !== 1 ? colspan : undefined, + rowspan: rowspan !== 1 ? rowspan : undefined, + colwidth: colwidth !== undefined && colwidth > 0 ? colwidth : undefined + }) + addNodes(builder, nodes) + builder.closeTag('th') + } else if (node.type === MarkupNodeType.comment) { + builder.addText('') + } else if (node.type === MarkupNodeType.embed) { + const src = toString(attrs.src) ?? '' + builder.openTag('a', { href: encodeURI(src), 'data-type': 'embed' }) + builder.addText(escapeHtml(src)) + builder.closeTag('a') + } else { + // Handle unknown node types as div with data attribute + builder.openTag('div', { 'data-node-type': node.type }) + addNodes(builder, nodes) + builder.closeTag('div') + } +} + +function addNode (builder: NodeBuilder, node: MarkupNode): void { + const marks = node.marks ?? [] + + if (marks.length > 0) { + addMarks(builder, marks, () => { + addNodeContent(builder, node) + }) + } else { + addNodeContent(builder, node) + } +} + +function toString (value: AttrValue | undefined): string | undefined { + return value !== undefined ? `${value}` : undefined +} + +function toNumber (value: AttrValue | undefined): number | undefined { + if (typeof value === 'boolean') { + return value ? 1 : 0 + } + + return value != null ? (typeof value === 'string' ? parseInt(value) : value) : undefined +} + +function toStyleAttr (attrs: Attrs): string | undefined { + const styles: string[] = [] + + if (attrs.textAlign != null) { + styles.push(`text-align: ${attrs.textAlign}`) + } + + return styles.length > 0 ? styles.join('; ') : undefined +} diff --git a/foundations/core/packages/text-html/tsconfig.json b/foundations/core/packages/text-html/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/text-html/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/text-markdown/.eslintrc.js b/foundations/core/packages/text-markdown/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/text-markdown/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/text-markdown/CHANGELOG.json b/foundations/core/packages/text-markdown/CHANGELOG.json new file mode 100644 index 0000000000..b155968395 --- /dev/null +++ b/foundations/core/packages/text-markdown/CHANGELOG.json @@ -0,0 +1,89 @@ +{ + "name": "@hcengineering/text-markdown", + "entries": [ + { + "version": "0.7.20", + "tag": "@hcengineering/text-markdown_v0.7.20", + "date": "Wed, 19 Nov 2025 07:37:47 GMT", + "comments": { + "patch": [ + { + "comment": "Fix NaN error in markdown" + } + ] + } + }, + { + "version": "0.7.19", + "tag": "@hcengineering/text-markdown_v0.7.19", + "date": "Thu, 06 Nov 2025 09:09:58 GMT", + "comments": { + "patch": [ + { + "comment": "Fix normalizeMarkdown errors" + } + ] + } + }, + { + "version": "0.7.18", + "tag": "@hcengineering/text-markdown_v0.7.18", + "date": "Mon, 27 Oct 2025 16:46:51 GMT", + "comments": { + "patch": [ + { + "comment": "add support for textColor and textStyle marks" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.17` to `0.7.18`" + }, + { + "comment": "Updating dependency \"@hcengineering/text-html\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/text-markdown_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/text-html\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/text-markdown_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/text-html\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/text-markdown/CHANGELOG.md b/foundations/core/packages/text-markdown/CHANGELOG.md new file mode 100644 index 0000000000..f6f944e867 --- /dev/null +++ b/foundations/core/packages/text-markdown/CHANGELOG.md @@ -0,0 +1,39 @@ +# Change Log - @hcengineering/text-markdown + +This log was last generated on Wed, 19 Nov 2025 07:37:47 GMT and should not be manually modified. + +## 0.7.20 +Wed, 19 Nov 2025 07:37:47 GMT + +### Patches + +- Fix NaN error in markdown + +## 0.7.19 +Thu, 06 Nov 2025 09:09:58 GMT + +### Patches + +- Fix normalizeMarkdown errors + +## 0.7.18 +Mon, 27 Oct 2025 16:46:51 GMT + +### Patches + +- add support for textColor and textStyle marks + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + diff --git a/foundations/core/packages/text-markdown/config/rig.json b/foundations/core/packages/text-markdown/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/text-markdown/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/text-markdown/jest.config.js b/foundations/core/packages/text-markdown/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/text-markdown/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/text-markdown/package.json b/foundations/core/packages/text-markdown/package.json new file mode 100644 index 0000000000..34d22bab56 --- /dev/null +++ b/foundations/core/packages/text-markdown/package.json @@ -0,0 +1,60 @@ +{ + "name": "@hcengineering/text-markdown", + "version": "0.7.20", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "build:watch": "compile", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/markdown-it": "~13.0.0", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/text-core": "workspace:^0.7.18", + "@hcengineering/text-html": "workspace:^0.7.18", + "markdown-it": "^14.0.0", + "fast-equals": "^5.2.2" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/text-markdown/src/__tests__/markdown.test.ts b/foundations/core/packages/text-markdown/src/__tests__/markdown.test.ts new file mode 100644 index 0000000000..1068e7b2ad --- /dev/null +++ b/foundations/core/packages/text-markdown/src/__tests__/markdown.test.ts @@ -0,0 +1,1252 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MarkupNode, MarkupNodeType, MarkupMarkType } from '@hcengineering/text-core' +import { markdownToMarkup, markupToMarkdown } from '..' +import { isMarkdownsEquals, normalizeMarkdown } from '../compare' + +const refUrl: string = 'ref://' +const imageUrl: string = 'http://localhost' +const options = { refUrl, imageUrl } + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toEqualMarkdown: (expected: string) => R + } + } +} + +expect.extend({ + toEqualMarkdown (received: string, expected: string) { + const pass = isMarkdownsEquals(received, expected) + return { + message: () => + pass + ? `Expected markdown strings NOT to be equal:\n Received:\n${received}\n Expected:\n${expected}` + : `Expected markdown strings to be equal:\n Received:\n${received}\n Expected:\n${expected}`, + pass + } + } +}) + +describe('markdownToMarkup', () => { + const tests: Array<{ name: string, markdown: string, markup: object }> = [ + { + name: 'simple text', + markdown: 'Lorem ipsum dolor sit amet.', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Lorem ipsum dolor sit amet.', + marks: [] + } + ] + } + ] + } + }, + { + name: 'text with heading', + markdown: `# Lorem ipsum + +Lorem ipsum dolor sit amet. +`, + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1, marker: '#' }, + content: [ + { + type: 'text', + text: 'Lorem ipsum', + marks: [] + } + ] + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Lorem ipsum dolor sit amet.', + marks: [] + } + ] + } + ] + } + }, + { + name: 'bullet list', + markdown: `# bullet list +- list item 1 +- list item 2 +`, + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1, marker: '#' }, + content: [ + { + type: 'text', + text: 'bullet list', + marks: [] + } + ] + }, + { + type: 'bulletList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'list item 1', + marks: [] + } + ] + } + ] + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'list item 2', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + }, + { + name: 'todos', + markdown: `# TODO +- [ ] todo 1 +- [x] todo 2 + `, + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1, marker: '#' }, + content: [ + { + type: 'text', + text: 'TODO', + marks: [] + } + ] + }, + { + type: 'todoList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'todoItem', + attrs: { checked: false }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo 1', + marks: [] + } + ] + } + ] + }, + { + type: 'todoItem', + attrs: { checked: true }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo 2', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + }, + { + name: 'todos followed by list items', + markdown: `# todo and list +- [ ] todo 1 +- [x] todo 2 +- list item 1 +- list item 2 +`, + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1, marker: '#' }, + content: [ + { + type: 'text', + text: 'todo and list', + marks: [] + } + ] + }, + { + type: 'todoList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'todoItem', + attrs: { checked: false }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo 1', + marks: [] + } + ] + } + ] + }, + { + type: 'todoItem', + attrs: { checked: true }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo 2', + marks: [] + } + ] + } + ] + } + ] + }, + { + type: 'bulletList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'list item 1', + marks: [] + } + ] + } + ] + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'list item 2', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + }, + { + name: 'todos followed by list items', + markdown: `# mixed lists +- [ ] todo 1 +- list item 1 +- [x] todo 2 +- list item 2 +`, + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1, marker: '#' }, + content: [ + { + type: 'text', + text: 'mixed lists', + marks: [] + } + ] + }, + { + type: 'todoList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'todoItem', + attrs: { checked: false }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo 1', + marks: [] + } + ] + } + ] + } + ] + }, + { + type: 'bulletList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'list item 1', + marks: [] + } + ] + } + ] + } + ] + }, + { + type: 'todoList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'todoItem', + attrs: { checked: true }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo 2', + marks: [] + } + ] + } + ] + } + ] + }, + { + type: 'bulletList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'list item 2', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + }, + { + name: 'nested todos', + markdown: `# nested todos +- [ ] todo + - [x] sub todo +`, + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1, marker: '#' }, + content: [ + { + type: 'text', + text: 'nested todos', + marks: [] + } + ] + }, + { + type: 'todoList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'todoItem', + attrs: { checked: false }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo', + marks: [] + } + ] + }, + { + type: 'todoList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'todoItem', + attrs: { checked: true }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub todo', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + { + name: 'nested lists', + markdown: `# nested lists +- [ ] todo + - sub list item + - [x] sub todo +- list item + - [x] sub todo + - sub list item +`, + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1, marker: '#' }, + content: [ + { + type: 'text', + text: 'nested lists', + marks: [] + } + ] + }, + { + type: 'todoList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'todoItem', + attrs: { checked: false }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo', + marks: [] + } + ] + }, + { + type: 'bulletList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub list item', + marks: [] + } + ] + } + ] + } + ] + }, + { + type: 'todoList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'todoItem', + attrs: { checked: true }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub todo', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + type: 'bulletList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'list item', + marks: [] + } + ] + }, + { + type: 'todoList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'todoItem', + attrs: { checked: true }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub todo', + marks: [] + } + ] + } + ] + } + ] + }, + { + type: 'bulletList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub list item', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + { + name: 'nested todos', + markdown: `# nested todos +- [ ] todo + - [x] sub todo +`, + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1, marker: '#' }, + content: [ + { + type: 'text', + text: 'nested todos', + marks: [] + } + ] + }, + { + type: 'todoList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'todoItem', + attrs: { checked: false }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo', + marks: [] + } + ] + }, + { + type: 'todoList', + attrs: { + bullet: '-' + }, + content: [ + { + type: 'todoItem', + attrs: { checked: true }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub todo', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + { + name: 'mermaid diagram', + markdown: '```mermaid\ngraph TD;\n\tA-->B;\n\tA-->C;\n\tB-->D;\n\tC-->D;\n```', + markup: { + type: 'doc', + content: [ + { + type: 'mermaid', + attrs: { + language: 'mermaid' + }, + content: [ + { + marks: [], + text: 'graph TD;\n\tA-->B;\n\tA-->C;\n\tB-->D;\n\tC-->D;', + type: 'text' + } + ] + } + ] + } + }, + { + name: 'embed', + markdown: 'http://localhost/embed', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'embed', + attrs: { src: 'http://localhost/embed' }, + content: [] + } + ] + } + ] + } + }, + { + name: 'embed-uri-escape', + markdown: + 'http://localhost/embed spaces', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'embed', + attrs: { src: 'http://localhost/embed spaces' }, + content: [] + } + ] + } + ] + } + }, + { + name: 'embed-html-escape', + markdown: + 'http://localhost/embed<html>', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'embed', + attrs: { src: 'http://localhost/embed' }, + content: [] + } + ] + } + ] + } + }, + { + name: 'multiline image alt', + markdown: '![line0\\\n\\\nline1](http://example.com/image.png)', + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'image', + attrs: { src: 'http://example.com/image.png', alt: 'line0\n\nline1' }, + content: [] + } + ] + } + ] + } + }, + { + name: 'image in a table cell', + markdown: + '

Some text

image-alt

', + markup: { + type: 'doc', + content: [ + { + type: 'table', + content: [ + { + type: 'tableRow', + content: [ + { + type: 'tableCell', + attrs: { + colspan: undefined, + rowspan: undefined, + colwidth: undefined + }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Some text' + } + ] + }, + { + type: 'paragraph', + attrs: { + textAlign: null + }, + content: [ + { + type: 'text', + text: ' ' + }, + { + type: 'image', + attrs: { + src: 'files/image_1.png', + alt: 'image-alt', + 'file-id': null, + title: null + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + { + name: 'textColor', + markdown: 'colored', + markup: { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'colored', + marks: [{ type: 'textColor' as MarkupMarkType, attrs: { color: '#abcdef' } }] + } + ] + } + ] + } + } + ] + + describe('to markup', () => { + tests.forEach(({ name, markdown, markup }) => { + it(name, () => { + const parsed = markdownToMarkup(markdown, options) + expect(parsed).toEqual(markup) + }) + }) + }) + + describe('to markdown', () => { + tests.forEach(({ name, markdown, markup }) => { + it(name, () => { + const serialized = markupToMarkdown(markup as MarkupNode, options) + expect(serialized).toEqualMarkdown(markdown) + }) + }) + }) +}) + +describe('markupToMarkdown', () => { + const tests: Array<{ name: string, markdown: string, markup: object }> = [ + { + name: 'links', + markdown: `[Link](https://example.com) + +[Link with spaces]() + +[Link with spaces and braces](>)`, + markup: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Link', + marks: [{ type: 'link', attrs: { href: 'https://example.com' } }] + } + ] + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Link with spaces', + marks: [{ type: 'link', attrs: { href: 'https://example.com/with spaces' } }] + } + ] + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Link with spaces and braces', + marks: [{ type: 'link', attrs: { href: 'https://example.com/' } }] + } + ] + } + ] + } + }, + { + name: 'textColor', + markdown: 'colored', + markup: { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'colored', + marks: [{ type: 'textColor' as MarkupMarkType, attrs: { color: '#abcdef' } }] + } + ] + } + ] + } + }, + { + name: 'textColor + italic', + markdown: '*styled*', + markup: { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'styled', + marks: [ + { type: 'textColor' as MarkupMarkType, attrs: { color: '#abcdef' } }, + { type: MarkupMarkType.em } + ] + } + ] + } + ] + } + }, + { + name: 'textStyle with font-family', + markdown: 'arial', + markup: { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'arial', + marks: [{ type: MarkupMarkType.textStyle, attrs: { fontFamily: 'Arial' } }] + } + ] + } + ] + } + }, + { + name: 'textStyle with multiple properties', + markdown: 'styled', + markup: { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'styled', + marks: [ + { + type: MarkupMarkType.textStyle, + attrs: { fontFamily: 'Arial', fontSize: '16px', fontWeight: 'bold' } + } + ] + } + ] + } + ] + } + }, + { + name: 'textStyle with color (no data-color)', + markdown: 'red', + markup: { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'red', + marks: [{ type: MarkupMarkType.textStyle, attrs: { color: 'red' } }] + } + ] + } + ] + } + } + ] + + describe('to markdown', () => { + tests.forEach(({ name, markdown, markup }) => { + it(name, () => { + const result = markupToMarkdown(markup as MarkupNode, options) + expect(result).toEqual(markdown) + }) + }) + }) +}) + +describe('markdownToMarkup -> markupToMarkdown', () => { + const tests: Array<{ name: string, markdown: string, alternate?: string }> = [ + { name: 'Italic', markdown: '*Asteriscs* and _Underscores_' }, + { name: 'Bold', markdown: '**Asteriscs** and __Underscores__' }, + { name: 'Bullet list with asteriscs', markdown: 'Asterisks :\r\n* Firstly\r\n* Secondly' }, + { name: 'Bullet list with dashes', markdown: 'Dashes :\r\n- Firstly\r\n- Secondly' }, + { name: 'TODO list with asteriscs', markdown: '* [ ] Take\n* [ ] Do\n\n' }, + { name: 'TODO list with dashes', markdown: '- [x] Take\n- [ ] Do\n\n' }, + { + name: 'Different markers', + markdown: + 'Asterisks bulleted list:\r\n* Asterisks: *Italic* and **Bold*** Underscores: _Italic_ and __Bold__\r\n\r\nDash bulleted list:\r\n- Asterisks: *Italic* and **Bold**\r\n- Underscores: _Italic_ and __Bold__\r\n-' + }, + { name: 'Single line comment', markdown: '' }, + { + name: 'Multsiline comment', + markdown: + '"\r\n\r\nThis PR fix issue #22424\r\n\r\n\r\n"' + }, + { + name: 'Link', + markdown: 'See [link](https://example.com)' + }, + { + name: 'Link with spaces', + markdown: 'See [link]()', + alternate: 'See [link](https://example.com/with%20spaces)' + }, + { + name: 'Link with spaces and braces', + markdown: 'See [link](>)', + alternate: 'See [link](https://example.com/%3Cwith%20spaces%3E)' + }, + { + name: 'Codeblock', + markdown: '```typescript\nconst x: number = 42;\n```' + }, + { + name: 'Image', + markdown: 'image' + }, + { + name: 'Images', + markdown: ` +Screenshot 2025-09-11 at 15 42 40 + +Screenshot 2025-09-11 at 15 43 42 + +Screenshot 2025-09-11 at 15 43 50` + }, + { + name: 'Image with multiline alt', + markdown: '![link0\\\n\\\nline1](http://example.com/image.png)' + }, + { + name: 'Table', + markdown: + '

Header 1

Header 2

Cell 1

Cell 2

Cell 3

Cell 4

' + }, + { + name: 'Complex table', + markdown: + '

Header

Cell 1

Cell 2

Cell 3

' + }, + { + name: 'Sub', + markdown: 'View in Huly TSK-50' + } + // { + // name: 'Malformed', + // markdown: 'try to parse me' + // } + ] + + tests.forEach(({ name, markdown, alternate }) => { + it(name, () => { + const json = markdownToMarkup(markdown, options) + const serialized = markupToMarkdown(json, options) + expect(serialized).toEqualMarkdown(alternate ?? markdown) + }) + }) +}) + +describe('normalizeMarkdown', () => { + it('handles null and undefined inputs', () => { + expect(normalizeMarkdown(null as any)).toBe('') + expect(normalizeMarkdown(undefined as any)).toBe('') + expect(normalizeMarkdown('')).toBe('') + }) + + it('handles non-string inputs', () => { + expect(normalizeMarkdown(123 as any)).toBe('') + expect(normalizeMarkdown({} as any)).toBe('') + expect(normalizeMarkdown([] as any)).toBe('') + }) + + it('normalizes line endings', () => { + expect(normalizeMarkdown('line1\r\nline2\rline3\nline4')).toBe('line1\nline2\nline3\nline4') + }) + + it('removes trailing whitespace and empty lines', () => { + expect(normalizeMarkdown('line1 \n \nline2 \n\n\nline3')).toBe('line1\nline2\nline3') + }) + + it('normalizes HTML tag attributes', () => { + const input = 'test' + const expected = 'test' + expect(normalizeMarkdown(input)).toBe(expected) + }) + + it('handles void elements correctly', () => { + expect(normalizeMarkdown('')).toBe('') + expect(normalizeMarkdown('
')).toBe('
') + expect(normalizeMarkdown('
')).toBe('
') + }) + + it('handles non-void elements correctly', () => { + expect(normalizeMarkdown('
')).toBe('
') + expect(normalizeMarkdown('')).toBe('') + }) + + it('sorts attributes alphabetically', () => { + const input = 'test' + const expected = 'test' + expect(normalizeMarkdown(input)).toBe(expected) + }) + + it('handles attributes without values', () => { + expect(normalizeMarkdown('')).toBe('') + }) + + it('handles mixed content', () => { + const input = 'Text before\r\ntest \n\nText after ' + const expected = 'Text before\ntest\nText after' + expect(normalizeMarkdown(input)).toBe(expected) + }) +}) diff --git a/foundations/core/packages/text-markdown/src/compare.ts b/foundations/core/packages/text-markdown/src/compare.ts new file mode 100644 index 0000000000..aef9fdb0a1 --- /dev/null +++ b/foundations/core/packages/text-markdown/src/compare.ts @@ -0,0 +1,126 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/** + * Calculate Sørensen–Dice coefficient + */ +export function calcSørensenDiceCoefficient (a: string, b: string): number { + if (a == null || b == null) return 0 + + const first = a.replace(/\s+/g, '') + const second = b.replace(/\s+/g, '') + + if (first === second) return 1 // identical or empty + if (first.length < 2 || second.length < 2) return 0 // if either is a 0-letter or 1-letter string + + const firstBigrams = new Map() + for (let i = 0; i < first.length - 1; i++) { + const bigram = first.substring(i, i + 2) + const count = (firstBigrams.get(bigram) ?? 0) + 1 + + firstBigrams.set(bigram, count) + } + + let intersectionSize = 0 + for (let i = 0; i < second.length - 1; i++) { + const bigram = second.substring(i, i + 2) + const count = firstBigrams.get(bigram) ?? 0 + + if (count > 0) { + firstBigrams.set(bigram, count - 1) + intersectionSize++ + } + } + + const denominator = first.length + second.length - 2 + if (denominator <= 0) return 0 + + return (2.0 * intersectionSize) / denominator +} + +/** + * Perform markdown diff/comparison to understand do we have a major differences. + */ +export function isMarkdownsEquals (source1: string, source2: string): boolean { + const normalized1 = normalizeMarkdown(source1) + const normalized2 = normalizeMarkdown(source2) + return normalized1 === normalized2 +} + +export function normalizeMarkdown (source: string): string { + if (source == null || typeof source !== 'string') return '' + + const tagRegex = /<(\w+)([^>]*?)(\/?)>/g + const attrRegex = /(\w+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g + + // Normalize line endings to LF + source = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n') + + // Remove extra blank lines + source = source + .split('\n') + .map((it) => it.trimEnd()) + .filter((it) => it.length > 0) + .join('\n') + + // Normalize HTML tags + source = source.replace(tagRegex, (match, tagName, attributes) => { + const attrs: Record = {} + + let attrMatch = attrRegex.exec(attributes) + while (attrMatch !== null) { + const attrName = attrMatch[1] + const attrValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? '' + attrs[attrName] = attrValue + attrMatch = attrRegex.exec(attributes) + } + + // Sort attributes by name for consistent order + const sortedAttrs = Object.keys(attrs) + .sort() + .map((key) => { + const value = attrs[key] + return value !== '' ? `${key}="${value}"` : key + }) + .join(' ') + + // Normalize to self-closing format for void elements + const voidElements = [ + 'img', + 'br', + 'hr', + 'input', + 'meta', + 'area', + 'base', + 'col', + 'embed', + 'link', + 'param', + 'source', + 'track', + 'wbr' + ] + const isVoidElement = voidElements.includes(tagName.toLowerCase()) + + if (sortedAttrs !== '') { + return isVoidElement ? `<${tagName} ${sortedAttrs} />` : `<${tagName} ${sortedAttrs}>` + } else { + return isVoidElement ? `<${tagName} />` : `<${tagName}>` + } + }) + + return source +} diff --git a/foundations/core/packages/text-markdown/src/index.ts b/foundations/core/packages/text-markdown/src/index.ts new file mode 100644 index 0000000000..7618c5548b --- /dev/null +++ b/foundations/core/packages/text-markdown/src/index.ts @@ -0,0 +1,47 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MarkupNode } from '@hcengineering/text-core' +import { MarkdownParser } from './parser' +import { MarkdownState, storeMarks, storeNodes } from './serializer' + +export * from './compare' +export * from './parser' +export * from './serializer' + +/** @public */ +export interface MarkdownOptions { + refUrl?: string + imageUrl?: string +} + +/** @public */ +export function markupToMarkdown (markup: MarkupNode, options?: MarkdownOptions): string { + const refUrl = options?.refUrl ?? 'ref://' + const imageUrl = options?.imageUrl ?? 'image://' + + const state = new MarkdownState(storeNodes, storeMarks, { tightLists: true, refUrl, imageUrl }) + state.renderContent(markup) + return state.out +} + +/** @public */ +export function markdownToMarkup (markdown: string, options?: MarkdownOptions): MarkupNode { + const refUrl = options?.refUrl ?? 'ref://' + const imageUrl = options?.imageUrl ?? 'image://' + + const parser = new MarkdownParser({ refUrl, imageUrl }) + return parser.parse(markdown ?? '') +} diff --git a/foundations/core/packages/text-markdown/src/marks.ts b/foundations/core/packages/text-markdown/src/marks.ts new file mode 100644 index 0000000000..66585ddddb --- /dev/null +++ b/foundations/core/packages/text-markdown/src/marks.ts @@ -0,0 +1,46 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MarkupMark, MarkupMarkType } from '@hcengineering/text-core' +import { deepEqual } from 'fast-equals' + +export function markAttrs (mark: MarkupMark): Record { + return mark.attrs ?? {} +} + +export function isInSet (mark: MarkupMark, marks: MarkupMark[]): boolean { + return marks.find((m) => markEq(mark, m)) !== undefined +} + +export function addToSet (mark: MarkupMark, marks: MarkupMark[]): MarkupMark[] { + const m = marks.find((m) => markEq(mark, m)) + if (m !== undefined) { + // We already have mark + return marks + } + return [...marks, mark] +} + +export function removeFromSet (markType: MarkupMarkType, marks: MarkupMark[]): MarkupMark[] { + return marks.filter((m) => m.type !== markType) +} + +export function sameSet (a?: MarkupMark[], b?: MarkupMark[]): boolean { + return deepEqual(a, b) +} + +export function markEq (first: MarkupMark, other: MarkupMark): boolean { + return deepEqual(first, other) +} diff --git a/foundations/core/packages/text-markdown/src/node.ts b/foundations/core/packages/text-markdown/src/node.ts new file mode 100644 index 0000000000..45c6f286bc --- /dev/null +++ b/foundations/core/packages/text-markdown/src/node.ts @@ -0,0 +1,24 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Attrs, MarkupNode } from '@hcengineering/text-core' + +export function nodeContent (node: MarkupNode): MarkupNode[] { + return node?.content ?? [] +} + +export function nodeAttrs (node: MarkupNode): Attrs { + return node.attrs ?? {} +} diff --git a/foundations/core/packages/text-markdown/src/parser.ts b/foundations/core/packages/text-markdown/src/parser.ts new file mode 100644 index 0000000000..6e71bae4cc --- /dev/null +++ b/foundations/core/packages/text-markdown/src/parser.ts @@ -0,0 +1,870 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Attrs, MarkupMark, MarkupMarkType, MarkupNode, MarkupNodeType } from '@hcengineering/text-core' +import { htmlToMarkup } from '@hcengineering/text-html' +import MarkdownIt, { type Token } from 'markdown-it' +import type { RuleCore } from 'markdown-it/lib/parser_core' +import type StateCore from 'markdown-it/lib/rules_core/state_core' + +import { addToSet, removeFromSet, sameSet } from './marks' +import { nodeContent } from './node' + +type SpecRule = T | ((tok: Token, state: MarkdownParseState) => T) + +function readSpec (rule: SpecRule, tok: Token, state: MarkdownParseState): T { + if (typeof rule === 'function') { + return (rule as (tok: Token, state: MarkdownParseState) => T)(tok, state) + } + return rule +} + +interface ParsingBlockRule { + block: SpecRule + getAttrs?: (tok: Token, state: MarkdownParseState) => Attrs + wrapContent?: boolean + noCloseToken?: boolean +} + +interface ParsingNodeRule { + node: MarkupNodeType + getAttrs?: (tok: Token, state: MarkdownParseState) => Attrs +} + +interface ParsingMarkRule { + mark: MarkupMarkType + getAttrs?: (tok: Token, state: MarkdownParseState) => Attrs + noCloseToken?: boolean +} + +interface ParsingSpecialRule { + type: (state: MarkdownParseState, tok: Token) => { type: MarkupMarkType | MarkupNodeType, node: boolean } | undefined + getAttrs?: (tok: Token, state: MarkdownParseState) => Attrs | undefined +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface ParsingIgnoreRule { + // empty +} + +type HandlerRecord = (state: MarkdownParseState, tok: Token) => void +type HandlersRecord = Record + +// **************************************************************** +// Markdown parser +// **************************************************************** +function isText (a: MarkupNode, b: MarkupNode): boolean { + return (a.type === MarkupNodeType.text || a.type === MarkupNodeType.reference) && b.type === MarkupNodeType.text +} +function maybeMerge (a: MarkupNode, b: MarkupNode): MarkupNode | undefined { + if (isText(a, b) && (sameSet(a.marks, b.marks) || (a.text === '' && (a.marks?.length ?? 0) === 0))) { + if (a.text === '' && (a.marks?.length ?? 0) === 0) { + return { ...b } + } + return { ...a, text: (a.text ?? '') + (b.text ?? '') } + } + return undefined +} + +interface StateElement { + type: MarkupNodeType + content: MarkupNode[] + attrs: Attrs +} + +// Object used to track the context of a running parse. +class MarkdownParseState { + stack: StateElement[] + marks: MarkupMark[] + tokenHandlers: Record void> + + constructor ( + tokenHandlers: Record void>, + readonly refUrl: string, + readonly imageUrl: string + ) { + this.stack = [{ type: MarkupNodeType.doc, attrs: {}, content: [] }] + this.marks = [] + this.tokenHandlers = tokenHandlers + } + + top (): StateElement | undefined { + return this.stack[this.stack.length - 1] + } + + push (elt: MarkupNode): void { + if (this.stack.length > 0) { + const tt = this.top() + tt?.content.push(elt) + } + } + + mergeWithLast (nodes: MarkupNode[], node: MarkupNode): boolean { + const last = nodes[nodes.length - 1] + let merged: MarkupNode | undefined + if (last !== undefined && (merged = maybeMerge(last, node)) !== undefined) { + nodes[nodes.length - 1] = merged + return true + } + return false + } + + // Adds the given text to the current position in the document, + // using the current marks as styling. + addText (text?: string): void { + const top = this.top() + if (text === undefined || top === undefined || text.length === 0) { + return + } + + const node: MarkupNode = { + type: MarkupNodeType.text, + text + } + if (this.marks !== undefined) { + node.marks = this.marks + } + + const nodes = top.content + + if (!this.mergeWithLast(nodes, node)) { + nodes.push(node) + } + } + + // Adds the given mark to the set of active marks. + openMark (mark: MarkupMark): void { + this.marks = addToSet(mark, this.marks) + } + + // Removes the given mark from the set of active marks. + closeMark (mark: MarkupMarkType): void { + this.marks = removeFromSet(mark, this.marks) + } + + parseTokens (toks: Token[] | null): void { + const _toks = [...(toks ?? [])] + while (_toks.length > 0) { + const tok = _toks.shift() + if (tok === undefined) { + break + } + + // Merge ... into one html token + if (tok.type === 'html_inline' && tok.content.trim() === '') { + while (_toks.length > 0) { + const _tok = _toks.shift() + if (_tok !== undefined) { + tok.content += _tok.content + if (_tok.type === 'html_inline' && _tok.content.trim() === '') { + break + } + } + } + } + + // Merge ... into one html token + if (tok.type === 'html_inline' && tok.content.trim().startsWith(' 0) { + const _tok = _toks.shift() + if (_tok !== undefined) { + tok.content += _tok.content + if (_tok.type === 'html_inline' && _tok.content.trim() === '') { + break + } + } + } + } + + const handler = this.tokenHandlers[tok.type] + if (handler === undefined) { + throw new Error(`Token type '${String(tok.type)} not supported by Markdown parser`) + } + handler(this, tok) + } + } + + // Add a node at the current position. + addNode (type: MarkupNodeType, attrs: Attrs, content: MarkupNode[] = []): MarkupNode { + const node: MarkupNode = { type, content } + + if (Object.keys(attrs ?? {}).length > 0) { + node.attrs = attrs + } + if (this.marks.length > 0) { + node.marks = this.marks + } + this.push(node) + return node + } + + // Wrap subsequent content in a node of the given type. + openNode (type: MarkupNodeType, attrs: Attrs): void { + this.stack.push({ type, attrs, content: [] }) + } + + // Close and return the node that is currently on top of the stack. + closeNode (): MarkupNode { + if (this.marks.length > 0) this.marks = [] + const info = this.stack.pop() + if (info !== undefined) { + return this.addNode(info.type, info.attrs, info.content) + } + return { type: MarkupNodeType.doc } + } +} + +function attrs ( + spec: ParsingBlockRule | ParsingMarkRule | ParsingNodeRule, + token: Token, + state: MarkdownParseState +): Attrs { + return spec.getAttrs?.(token, state) ?? {} +} + +// Code content is represented as a single token with a `content` +// property in Markdown-it. +function noCloseToken (spec: ParsingBlockRule | ParsingMarkRule, type: string): boolean { + return (spec.noCloseToken ?? false) || ['code_inline', 'code_block', 'fence'].indexOf(type) > 0 +} + +function withoutTrailingNewline (str: string): string { + return str[str.length - 1] === '\n' ? str.slice(0, str.length - 1) : str +} + +function addSpecBlock ( + handlers: HandlersRecord, + spec: ParsingBlockRule, + type: string, + specBlock: SpecRule +): void { + if (noCloseToken(spec, type)) { + handlers[type] = newSimpleBlockHandler(specBlock, spec) + } else { + handlers[type + '_open'] = (state, tok) => { + state.openNode(readSpec(specBlock, tok, state), attrs(spec, tok, state)) + if (spec.wrapContent === true) { + state.openNode(MarkupNodeType.paragraph, {}) + } + } + handlers[type + '_close'] = (state) => { + if (spec.wrapContent === true) { + state.closeNode() + } + state.closeNode() + } + } +} +function newSimpleBlockHandler (specBlock: SpecRule, spec: ParsingBlockRule): HandlerRecord { + return (state, tok) => { + state.openNode(readSpec(specBlock, tok, state), attrs(spec, tok, state)) + state.addText(withoutTrailingNewline(tok.content)) + state.closeNode() + } +} + +function addSpecMark (handlers: HandlersRecord, spec: ParsingMarkRule, type: string, specMark: MarkupMarkType): void { + if (noCloseToken(spec, type)) { + handlers[type] = newSimpleMarkHandler(spec, specMark) + } else { + handlers[type + '_open'] = (state, tok) => { + state.openMark({ type: specMark, attrs: attrs(spec, tok, state) }) + } + handlers[type + '_close'] = (state) => { + state.closeMark(specMark) + } + } +} +function addSpecialRule (handlers: HandlersRecord, spec: ParsingSpecialRule, type: string): void { + handlers[type + '_open'] = (state, tok) => { + const type = spec.type(state, tok) + if (type !== undefined) { + if (type.node) { + state.openNode(type.type as MarkupNodeType, spec.getAttrs?.(tok, state) ?? {}) + } else { + state.openMark({ type: type.type as MarkupMarkType, attrs: spec.getAttrs?.(tok, state) ?? {} }) + } + } + } + handlers[type + '_close'] = (state, tok) => { + const type = spec.type(state, tok) + if (type !== undefined) { + if (type.node) { + state.closeNode() + } else { + state.closeMark(type.type as MarkupMarkType) + } + } + } +} +function addIgnoreRule (handlers: HandlersRecord, spec: ParsingIgnoreRule, type: string): void { + handlers[type + '_open'] = (state, tok) => {} + handlers[type + '_close'] = (state, tok) => {} +} +function newSimpleMarkHandler (spec: ParsingMarkRule, specMark: MarkupMarkType): HandlerRecord { + return (state: MarkdownParseState, tok: Token): void => { + state.openMark({ attrs: attrs(spec, tok, state), type: specMark }) + state.addText(withoutTrailingNewline(tok.content)) + state.closeMark(specMark) + } +} + +function tokenHandlers ( + tokensBlock: Record, + tokensNode: Record, + tokensMark: Record, + specialRules: Record, + ignoreRules: Record, + htmlParser: HtmlParser +): HandlersRecord { + const handlers: HandlersRecord = {} + + Object.entries(tokensBlock).forEach(([type, spec]) => { + addSpecBlock(handlers, spec, type, spec.block) + }) + Object.entries(tokensNode).forEach(([type, spec]) => { + addSpecNode(handlers, type, spec) + }) + Object.entries(tokensMark).forEach(([type, spec]) => { + addSpecMark(handlers, spec, type, spec.mark) + }) + Object.entries(specialRules).forEach(([type, spec]) => { + addSpecialRule(handlers, spec, type) + }) + Object.entries(ignoreRules).forEach(([type, spec]) => { + addIgnoreRule(handlers, spec, type) + }) + + handlers.html_inline = (state: MarkdownParseState, tok: Token) => { + try { + const top = state.top() + if (tok.content.trim() === '' && top?.type === MarkupNodeType.embed) { + top.content = [] + state.closeNode() + return + } + const markup = htmlParser(tok.content) + if (markup.content !== undefined) { + // unwrap content from wrapping paragraph + const shouldUnwrap = + markup.content.length === 1 && + markup.content[0].type === MarkupNodeType.paragraph && + top?.type === MarkupNodeType.paragraph + + const content = nodeContent(shouldUnwrap ? markup.content[0] : markup) + for (const c of content) { + if (c.type === MarkupNodeType.embed) { + state.openNode(MarkupNodeType.embed, c.attrs ?? {}) + continue + } + state.push(c) + } + } + } catch (err: any) { + console.error(err) + state.addText(tok.content) + } + } + handlers.html_block = (state: MarkdownParseState, tok: Token) => { + try { + const model = htmlParser(tok.content) + const content = nodeContent(model) + for (const c of content) { + state.push(c) + } + } catch (err: any) { + console.error(err) + state.addText(tok.content) + } + } + + addTextHandlers(handlers) + + return handlers +} + +function addTextHandlers (handlers: HandlersRecord): void { + handlers.text = (state, tok) => { + state.addText(tok.content) + } + handlers.inline = (state, tok) => { + state.parseTokens(tok.children) + } + handlers.softbreak = (state) => { + state.addText('\n') + } +} + +function addSpecNode (handlers: HandlersRecord, type: string, spec: ParsingNodeRule): void { + handlers[type] = (state: MarkdownParseState, tok: Token) => state.addNode(spec.node, attrs(spec, tok, state)) +} + +function tokAttrGet (token: Token, name: string): string | undefined { + const attr = token.attrGet(name) + if (attr != null) { + return attr + } + // try iterate attrs + for (const [k, v] of token.attrs ?? []) { + if (k === name) { + return v + } + } +} + +function tokToAttrs (token: Token, ...names: string[]): Record { + const result: Record = {} + for (const name of names) { + const attr = token.attrGet(name) + if (attr !== null) { + result[name] = attr + } + } + return result +} + +function todoItemMetaAttrsGet (tok: Token): Record { + const userid = tokAttrGet(tok, 'userid') + const todoid = tokAttrGet(tok, 'todoid') + + const result: Record = {} + + if (userid !== undefined) { + result.userid = userid + } + if (todoid !== undefined) { + result.todoid = todoid + } + + return result +} + +// ::- A configuration of a Markdown parser. Such a parser uses +const tokensBlock: Record = { + blockquote: { block: MarkupNodeType.blockquote }, + paragraph: { block: MarkupNodeType.paragraph }, + list_item: { block: MarkupNodeType.list_item }, + task_item: { block: MarkupNodeType.taskItem, getAttrs: (tok) => ({ 'data-type': 'taskItem' }) }, + bullet_list: { + block: MarkupNodeType.bullet_list, + getAttrs: (tok) => ({ + bullet: tok.markup + }) + }, + todo_list: { + block: MarkupNodeType.todoList, + getAttrs: (tok) => ({ + bullet: tok.markup + }) + }, + todo_item: { + block: MarkupNodeType.todoItem, + getAttrs: (tok) => ({ + checked: tokAttrGet(tok, 'checked') === 'true', + ...todoItemMetaAttrsGet(tok) + }) + }, + ordered_list: { + block: MarkupNodeType.ordered_list, + getAttrs: (tok: Token) => ({ order: tokAttrGet(tok, 'start') ?? '1' }) + }, + task_list: { + block: MarkupNodeType.taskList, + getAttrs: (tok: Token) => ({ order: tokAttrGet(tok, 'start') ?? '1', 'data-type': 'taskList' }) + }, + heading: { + block: MarkupNodeType.heading, + getAttrs: (tok: Token) => ({ level: Number(tok.tag.slice(1)), marker: tok.markup }) + }, + code_block: { + block: (tok) => { + if (tok.info === 'mermaid') { + return MarkupNodeType.mermaid + } + return MarkupNodeType.code_block + }, + getAttrs: (tok: Token) => { + return { language: tok.info ?? '' } + }, + noCloseToken: true + }, + fence: { + block: (tok) => { + if (tok.info === 'mermaid') { + return MarkupNodeType.mermaid + } + return MarkupNodeType.code_block + }, + getAttrs: (tok: Token) => { + return { language: tok.info ?? '' } + }, + noCloseToken: true + }, + sub: { + block: MarkupNodeType.subLink, + getAttrs: (tok: Token) => { + return { language: tok.info ?? '' } + }, + noCloseToken: false + }, + table: { + block: MarkupNodeType.table, + noCloseToken: false + }, + th: { + block: MarkupNodeType.table_header, + getAttrs: (tok: Token) => { + return { + colspan: Number(tok.attrGet('colspan') ?? '1'), + rowspan: Number(tok.attrGet('rowspan') ?? '1') + } + }, + wrapContent: true, + noCloseToken: false + }, + tr: { + block: MarkupNodeType.table_row, + noCloseToken: false + }, + td: { + block: MarkupNodeType.table_cell, + getAttrs: (tok: Token) => { + return { + colspan: Number(tok.attrGet('colspan') ?? '1'), + rowspan: Number(tok.attrGet('rowspan') ?? '1') + } + }, + wrapContent: true, + noCloseToken: false + } +} +const tokensNode: Record = { + hr: { node: MarkupNodeType.horizontal_rule }, + image: { + node: MarkupNodeType.image, + getAttrs: (tok: Token, state) => { + const result = tokToAttrs(tok, 'src', 'title', 'alt', 'data') + result.alt = convertStringLikeToken(tok, result.alt) + if (result.src.startsWith(state.imageUrl)) { + const url = new URL(result.src) + result['data-type'] = 'image' + const file = url.searchParams.get('file') + if (file != null) { + result['file-id'] = file + } + + const width = url.searchParams.get('width') + if (width != null) { + result.width = width + } + + const height = url.searchParams.get('height') + if (height != null) { + result.height = height + } + } + return result + } + }, + hardbreak: { node: MarkupNodeType.hard_break } +} +const tokensMark: Record = { + em: { + mark: MarkupMarkType.em, + getAttrs: (tok: Token, state: MarkdownParseState) => { + return { marker: tok.markup } + } + }, + bold: { + mark: MarkupMarkType.bold, + getAttrs: (tok: Token, state: MarkdownParseState) => { + return { marker: tok.markup } + } + }, + strong: { + mark: MarkupMarkType.bold, + getAttrs: (tok: Token, state: MarkdownParseState) => { + return { marker: tok.markup } + } + }, + s: { mark: MarkupMarkType.strike }, + u: { mark: MarkupMarkType.underline }, + code_inline: { + mark: MarkupMarkType.code, + noCloseToken: true + } +} + +const specialRule: Record = { + link: { + type: (state, tok) => { + const href = tok.attrGet('href') + if ((href?.startsWith(state.refUrl) ?? false) || state.stack[state.stack.length - 1]?.type === 'reference') { + return { type: MarkupNodeType.reference, node: true } + } + return { type: MarkupMarkType.link, node: false, close: true } + }, + getAttrs: (tok: Token, state) => { + const attrs = tokToAttrs(tok, 'href', 'title') + if (attrs.href !== undefined) { + try { + const url = new URL(attrs.href) + if (attrs.href.startsWith(state.refUrl) ?? false) { + return { + label: url.searchParams?.get('label') ?? '', + id: url.searchParams?.get('_id') ?? '', + objectclass: url.searchParams?.get('_class') ?? '' + } + } + } catch (err: any) { + // ignore + } + } + return attrs + } + } +} + +const ignoreRule: Record = { + thead: {}, + tbody: {} +} + +export const isInlineToken = (token?: Token): boolean => token?.type === 'inline' + +export const isParagraphToken = (token?: Token): boolean => token?.type === 'paragraph_open' + +export const isListItemToken = (token?: Token): boolean => token?.type === 'list_item_open' + +export interface TaskListEnv { + tasklists: number +} + +interface TaskListStateCore extends StateCore { + env: TaskListEnv +} + +// The leading whitespace in a list item (token.content) is already trimmed off by markdown-it. +// The regex below checks for '[ ] ' or '[x] ' or '[X] ' at the start of the string token.content, +// where the space is either a normal space or a non-breaking space (character 160 = \u00A0). +const startsWithTodoMarkdown = (token: Token): boolean => /^\[[xX \u00A0]\][ \u00A0]/.test(token.content) +const isCheckedTodoItem = (token: Token): boolean => /^\[[xX]\][ \u00A0]/.test(token.content) + +export type HtmlParser = (html: string) => MarkupNode + +export interface MarkdownParserOptions { + refUrl: string + imageUrl: string + htmlParser?: HtmlParser +} + +export class MarkdownParser { + tokenizer: MarkdownIt + tokenHandlers: Record void> + htmlParser: HtmlParser + + constructor (private readonly options: MarkdownParserOptions) { + this.tokenizer = MarkdownIt('default', { + html: true + }) + this.tokenizer.core.ruler.after('inline', 'task_list', this.listRule) + this.tokenizer.core.ruler.after('inline', 'html_comment', this.htmlCommentRule) + + this.htmlParser = options.htmlParser ?? htmlToMarkup + this.tokenHandlers = tokenHandlers(tokensBlock, tokensNode, tokensMark, specialRule, ignoreRule, this.htmlParser) + } + + parse (text: string): MarkupNode { + const state = new MarkdownParseState(this.tokenHandlers, this.options.refUrl, this.options.imageUrl) + let doc: MarkupNode + + const tokens = this.tokenizer.parse(text, {}) + + state.parseTokens(tokens) + do { + doc = state.closeNode() + } while (state.stack.length > 0) + return doc + } + + htmlCommentRule: RuleCore = (state: StateCore): boolean => { + const tokens = state.tokens + for (let i = 0; i < tokens.length; i++) { + // Prosemirror entirely ignores comments when parsing, so + // here we replaces html comment tag with a custom tag so the comments got parsed as a node + if (tokens[i].type === 'html_block' || tokens[i].type === 'html_inline') { + const content = tokens[i].content.replaceAll('', '') + tokens[i].content = content + } + } + return true + } + + listRule: RuleCore = (state: TaskListStateCore): boolean => { + const tokens = (state as any).tokens + const states: Array<{ closeIdx: number, lastItemIdx: number }> = [] + + // step #1 - convert list items to todo items + for (let open = 0; open < tokens.length; open++) { + if (isTodoListItem(tokens, open)) { + convertTodoItem(tokens, open) + } + } + + // step #2 - convert lists to proper type + // listCloseIdx and itemCloseIdx tracks position of the list and item close tokens + // because we insert items into the list, the variables keep the position from the + // end of the list so we don't have to count inserts + let listCloseIdx = -1 + let itemCloseIdx = -1 + + for (let i = tokens.length - 1; i >= 0; i--) { + if (tokens[i].type === 'bullet_list_close') { + states.push({ closeIdx: listCloseIdx, lastItemIdx: itemCloseIdx }) + listCloseIdx = tokens.length - i + itemCloseIdx = -1 + } else if (tokens[i].type === 'list_item_close' || tokens[i].type === 'todo_item_close') { + // when found item close token of different type, split the list + if (itemCloseIdx === -1) { + itemCloseIdx = tokens.length - i + } else if (tokens[i].type !== tokens[tokens.length - itemCloseIdx].type) { + const bulletListOpen = new state.Token('bullet_list_open', 'ul', 1) + bulletListOpen.markup = tokens[i + 1].markup + tokens.splice(i + 1, 0, bulletListOpen) + tokens.splice(i + 1, 0, new state.Token('bullet_list_close', 'ul', -1)) + convertTodoList(tokens, i + 2, tokens.length - listCloseIdx, tokens.length - itemCloseIdx) + listCloseIdx = tokens.length - i - 1 + itemCloseIdx = tokens.length - i + } + } else if (tokens[i].type === 'bullet_list_open') { + if (itemCloseIdx !== -1) { + convertTodoList(tokens, i, tokens.length - listCloseIdx, tokens.length - itemCloseIdx) + } + + const prevState = states.pop() ?? { closeIdx: -1, lastItemIdx: -1 } + listCloseIdx = prevState.closeIdx + itemCloseIdx = prevState.lastItemIdx + } + } + + return true + } +} + +function convertTodoList (tokens: Token[], open: number, close: number, item: number): void { + if (tokens[open].type !== 'bullet_list_open') { + throw new Error('bullet_list_open token expected') + } + if (tokens[close].type !== 'bullet_list_close') { + throw new Error('bullet_list_close token expected') + } + + if (tokens[item].type === 'todo_item_close') { + tokens[open].type = 'todo_list_open' + tokens[close].type = 'todo_list_close' + } +} + +function convertTodoItem (tokens: Token[], open: number): boolean { + const close = findListItemCloseToken(tokens, open) + if (close !== -1) { + tokens[open].type = 'todo_item_open' + tokens[close].type = 'todo_item_close' + + const inline = tokens[open + 2] + + if (tokens[open].attrs == null) { + tokens[open].attrs = [] + } + + ;(tokens[open].attrs as any).push(['checked', isCheckedTodoItem(inline) ? 'true' : 'false']) + + if (inline.children !== null) { + const newContent = inline.children[0].content.slice(4) + if (newContent.length > 0) { + inline.children[0].content = newContent + } else { + inline.children = inline.children.slice(1) + } + + const metaTok = inline.children.find( + (tok: Token) => tok.type === 'html_inline' && tok.content.startsWith('') + ) + if (metaTok !== undefined) { + const metaValues = metaTok.content.slice(5, -4).split(',') + for (const mv of metaValues) { + if (mv.startsWith('todoid')) { + ;(tokens[open].attrs as any).push(['todoid', mv.slice(7)]) + } + if (mv.startsWith('userid')) { + ;(tokens[open].attrs as any).push(['userid', mv.slice(7)]) + } + } + } + } + + return true + } + + return false +} + +function findListItemCloseToken (tokens: Token[], open: number): number { + if (tokens[open].type !== 'list_item_open') { + throw new Error('list_item_open token expected') + } + + const level = tokens[open].level + for (let close = open + 1; close < tokens.length; close++) { + if (tokens[close].type === 'list_item_close' && tokens[close].level === level) { + return close + } + } + + return -1 +} + +// todo token structure +// tokens[i].type === list_item_open +// tokens[i + 1].type === paragraph +// tokens[i + 2].type === inline +function isTodoListItem (tokens: Token[], pos: number): boolean { + return ( + isListItemToken(tokens[pos]) && + isParagraphToken(tokens[pos + 1]) && + isInlineToken(tokens[pos + 2]) && + startsWithTodoMarkdown(tokens[pos + 2]) + ) +} + +function convertStringLikeToken (tok: Token, attrValue?: string): string { + if (typeof attrValue === 'string' && attrValue !== '') { + return attrValue + } + const children = tok.children ?? [] + let out = '' + for (const child of children) { + switch (child.type) { + case 'text': + out += child.content + break + case 'hardbreak': + out += '\n' + break + } + } + + return out +} diff --git a/foundations/core/packages/text-markdown/src/serializer.ts b/foundations/core/packages/text-markdown/src/serializer.ts new file mode 100644 index 0000000000..ed490608ce --- /dev/null +++ b/foundations/core/packages/text-markdown/src/serializer.ts @@ -0,0 +1,876 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MarkupMark, MarkupNode, MarkupNodeType } from '@hcengineering/text-core' +import { markupToHtml } from '@hcengineering/text-html' + +import { isInSet, markEq } from './marks' +import { nodeContent, nodeAttrs } from './node' + +type FirstDelim = (i: number, attrs?: Record, parentAttrs?: Record) => string +interface IState { + wrapBlock: (delim: string, firstDelim: string | null, node: MarkupNode, f: () => void) => void + flushClose: (size: number) => void + atBlank: () => void + ensureNewLine: () => void + write: (content: string) => void + closeBlock: (node: any) => void + text: (text: string, escape?: boolean) => void + render: (node: MarkupNode, parent: MarkupNode, index: number) => void + renderContent: (parent: MarkupNode) => void + renderInline: (parent: MarkupNode) => void + renderList: (node: MarkupNode, delim: string, firstDelim: FirstDelim) => void + esc: (str: string, startOfLine?: boolean) => string + htmlEsc: (str: string) => string + quote: (str: string) => string + repeat: (str: string, n: number) => string + markString: (mark: MarkupMark, open: boolean, parent: MarkupNode, index: number) => string + renderHtml: (node: MarkupNode) => string + refUrl: string + imageUrl: string + inAutolink?: boolean + renderAHref?: boolean +} + +type NodeProcessor = (state: IState, node: MarkupNode, parent: MarkupNode, index: number) => void + +interface InlineState { + active: MarkupMark[] + trailing: string + parent: MarkupNode + node?: MarkupNode + marks: MarkupMark[] +} + +// ************************************************************* + +function backticksFor (side: boolean): string { + return side ? '`' : '`' +} + +function isPlainURL (link: MarkupMark, parent: MarkupNode, index: number): boolean { + if (link.attrs?.title !== undefined || !/^\w+:/.test(link.attrs?.href)) return false + const content = parent.content?.[index] + if (content === undefined) { + return false + } + if ( + content.type !== MarkupNodeType.text || + content.text !== link.attrs?.href || + content.marks?.[content.marks.length - 1] !== link + ) { + return false + } + return index === (parent.content?.length ?? 0) - 1 || !isInSet(link, parent.content?.[index + 1]?.marks ?? []) +} + +const formatTodoItem: FirstDelim = (i, attrs, parentAttrs?: Record) => { + const meta = + attrs?.todoid !== undefined && attrs?.userid !== undefined + ? `` + : '' + + const bullet = parentAttrs?.bullet ?? '*' + return `${bullet} [${attrs?.checked === true ? 'x' : ' '}] ${meta}` +} + +// ************************************************************* + +export const storeNodes: Record = { + blockquote: (state, node) => { + state.wrapBlock('> ', null, node, () => { + state.renderContent(node) + }) + }, + codeBlock: (state, node) => { + state.write('```' + `${nodeAttrs(node).language ?? ''}` + '\n') + // TODO: Check for node.textContent + state.renderInline(node) + // state.text(node.text ?? '', false) + state.ensureNewLine() + state.write('```') + state.closeBlock(node) + }, + mermaid: (state, node) => { + state.write('```mermaid\n') + state.renderInline(node) + state.ensureNewLine() + state.write('```') + state.closeBlock(node) + }, + heading: (state, node) => { + const attrs = nodeAttrs(node) + if (attrs.marker === '=' && attrs.level === 1) { + state.renderInline(node) + state.ensureNewLine() + state.write('===\n') + } else if (attrs.marker === '-' && attrs.level === 2) { + state.renderInline(node) + state.ensureNewLine() + state.write('---\n') + } else { + state.write(state.repeat('#', attrs.level !== undefined ? Number(attrs.level) : 1) + ' ') + state.renderInline(node) + } + state.closeBlock(node) + }, + horizontalRule: (state, node) => { + state.write(`${nodeAttrs(node).markup ?? '---'}`) + state.closeBlock(node) + }, + bulletList: (state, node) => { + state.renderList(node, ' ', () => `${nodeAttrs(node).bullet ?? '*'}` + ' ') + }, + taskList: (state, node) => { + state.renderList(node, ' ', () => '* [ ]' + ' ') + }, + todoList: (state, node) => { + state.renderList(node, ' ', formatTodoItem) + }, + orderedList: (state, node) => { + let start = 1 + if (nodeAttrs(node).order !== undefined) { + start = Number(nodeAttrs(node).order) + } + const maxW = String(start + nodeContent(node).length - 1).length + const space = state.repeat(' ', maxW + 2) + state.renderList(node, space, (i: number) => { + const nStr = String(start + i) + return state.repeat(' ', maxW - nStr.length) + nStr + '. ' + }) + }, + listItem: (state, node) => { + state.renderContent(node) + }, + taskItem: (state, node) => { + state.renderContent(node) + }, + todoItem: (state, node) => { + state.renderContent(node) + }, + paragraph: (state, node) => { + state.renderInline(node) + state.closeBlock(node) + }, + subLink: (state, node) => { + state.write('') + state.renderAHref = true + state.renderInline(node) + state.renderAHref = false + state.write('') + }, + + image: (state, node) => { + const attrs = nodeAttrs(node) + if (attrs.token != null && attrs['file-id'] != null) { + // Convert image to token format + state.write( + '![' + + state.esc(`${attrs.alt ?? ''}`) + + '](' + + (state.imageUrl + + `${attrs['file-id']}` + + `?file=${attrs['file-id']}` + + (attrs.width != null ? '&width=' + state.esc(`${attrs.width}`) : '') + + (attrs.height != null ? '&height=' + state.esc(`${attrs.height}`) : '') + + (attrs.token != null ? '&token=' + state.esc(`${attrs.token}`) : '')) + + (attrs.title != null ? ' ' + state.quote(`${attrs.title}`) : '') + + ')' + ) + } else if (attrs['file-id'] != null) { + // Convert image to fileid format + state.write( + '![' + + state.esc(`${attrs.alt ?? ''}`) + + '](' + + (state.imageUrl + + `${attrs['file-id']}` + + (attrs.width != null ? '&width=' + state.esc(`${attrs.width}`) : '') + + (attrs.height != null ? '&height=' + state.esc(`${attrs.height}`) : '')) + + (attrs.title != null ? ' ' + state.quote(`${attrs.title}`) : '') + + ')' + ) + } else { + if (attrs.width != null || attrs.height != null) { + state.write( + '' + state.quote(`${attrs.title}`) + '' : '>') + ) + } else { + state.write( + '![' + + state.esc(`${attrs.alt ?? ''}`) + + '](' + + state.esc(`${attrs.src}`) + + (attrs.title != null ? ' ' + state.quote(`${attrs.title}`) : '') + + ')' + ) + } + } + }, + reference: (state, node) => { + const attrs = nodeAttrs(node) + let url = state.refUrl + if (!url.includes('?')) { + url += '?' + } else { + url += '&' + } + state.write( + '[' + + state.esc(`${attrs.label ?? ''}`) + + '](' + + `${url}${makeQuery({ + _class: attrs.objectclass, + _id: attrs.id, + label: attrs.label + })}` + + (attrs.title !== undefined ? ' ' + state.quote(`${attrs.title}`) : '') + + ')' + ) + }, + markdown: (state, node) => { + state.renderInline(node) + state.closeBlock(node) + }, + comment: (state, node) => { + state.write('') + }, + hardBreak: (state, node, parent, index) => { + const content = nodeContent(parent) + for (let i = index + 1; i < content.length; i++) { + if (content[i].type !== node.type) { + state.write('\\\n') + return + } + } + }, + text: (state, node) => { + // Check if test has reference mark, in this case we need to remove [[]] + state.text(node.text ?? '') + }, + emoji: (state, node) => { + state.text(node.attrs?.emoji as string) + }, + table: (state, node) => { + state.write(state.renderHtml(node)) + state.closeBlock(node) + }, + embed: (state, node) => { + const attrs = nodeAttrs(node) + const embedUrl = attrs.src as string + state.write(``) + // Slashes are escaped to prevent autolink creation + state.write(state.htmlEsc(embedUrl).replace(/\//g, '/')) + state.write('') + } +} + +interface MarkProcessor { + open: ((_state: IState, mark: MarkupMark, parent: MarkupNode, index: number) => string) | string + close: ((_state: IState, mark: MarkupMark, parent: MarkupNode, index: number) => string) | string + mixable: boolean + expelEnclosingWhitespace: boolean + escape: boolean +} + +export const storeMarks: Record = { + em: { + open: '*', + close: '*', + mixable: true, + expelEnclosingWhitespace: true, + escape: true + }, + italic: { + open: '*', + close: '*', + mixable: true, + expelEnclosingWhitespace: true, + escape: true + }, + bold: { + open: '**', + close: '**', + mixable: true, + expelEnclosingWhitespace: true, + escape: true + }, + strong: { + open: '**', + close: '**', + mixable: true, + expelEnclosingWhitespace: true, + escape: true + }, + strike: { + open: '~~', + close: '~~', + mixable: true, + expelEnclosingWhitespace: true, + escape: true + }, + underline: { + open: '', + close: '', + mixable: true, + expelEnclosingWhitespace: true, + escape: true + }, + link: { + open: (state, mark, parent, index) => { + if (state.renderAHref === true) { + return `` + } else { + state.inAutolink = isPlainURL(mark, parent, index) + return state.inAutolink ? '<' : '[' + } + }, + close: (state, mark, parent, index) => { + if (state.renderAHref === true) { + return '' + } else { + const { inAutolink } = state + state.inAutolink = undefined + + const href = (mark.attrs?.href as string) ?? '' + // eslint-disable-next-line + const url = href.replace(/[\(\)"\\<>]/g, '\\$&') + const hasSpaces = url.includes(' ') + + return inAutolink === true + ? '>' + : '](' + + (hasSpaces ? `<${url}>` : url) + + (mark.attrs?.title !== undefined ? ` "${(mark.attrs?.title as string).replace(/"/g, '\\"')}"` : '') + + ')' + } + }, + mixable: false, + expelEnclosingWhitespace: false, + escape: true + }, + code: { + open: (state, mark, parent, index) => { + return backticksFor(false) + }, + close: (state, mark, parent, index) => { + return backticksFor(true) + }, + mixable: false, + expelEnclosingWhitespace: false, + escape: false + }, + textColor: { + open: (state, mark, parent, index) => { + if (mark.attrs?.color === undefined) { + return '' + } + return `` + }, + close: (state, mark, parent, index) => { + if (mark.attrs?.color === undefined) { + return '' + } + return '' + }, + mixable: false, + expelEnclosingWhitespace: false, + escape: false + }, + textStyle: { + open: (state, mark, parent, index) => { + const attrs = mark.attrs ?? {} + if (Object.keys(attrs).length === 0) { + return '' + } + const styleAttrs = Object.entries(attrs) + .map(([key, value]) => { + const kebabKey = key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`) + return `${kebabKey}: ${value}` + }) + .join('; ') + + return `` + }, + close: (state, mark, parent, index) => { + const attrs = mark.attrs ?? {} + if (Object.keys(attrs).length === 0) { + return '' + } + return '' + }, + mixable: false, + expelEnclosingWhitespace: false, + escape: false + } +} + +export type HtmlWriter = (markup: MarkupNode) => string + +export interface StateOptions { + tightLists: boolean + refUrl: string + imageUrl: string + htmlWriter?: HtmlWriter +} +export class MarkdownState implements IState { + nodes: Record + marks: Record + delim: string + out: string + closed: boolean + closedNode?: MarkupNode + inTightList: boolean + options: StateOptions + refUrl: string + imageUrl: string + htmlWriter: HtmlWriter + + constructor ( + nodes = storeNodes, + marks = storeMarks, + options: StateOptions = { tightLists: true, refUrl: 'ref://', imageUrl: 'http://' } + ) { + this.nodes = nodes + this.marks = marks + this.delim = this.out = '' + this.closed = false + this.inTightList = false + this.refUrl = options.refUrl + this.imageUrl = options.imageUrl + this.htmlWriter = options.htmlWriter ?? markupToHtml + + this.options = options + } + + flushClose (size: number): void { + if (this.closed) { + if (!this.atBlank()) this.out += '\n' + if (size > 1) { + this.addDelim(size) + } + this.closed = false + } + } + + private addDelim (size: number): void { + let delimMin = this.delim + const trim = /\s+$/.exec(delimMin) + if (trim !== null) { + delimMin = delimMin.slice(0, delimMin.length - trim[0].length) + } + for (let i = 1; i < size; i++) { + this.out += delimMin + '\n' + } + } + + renderHtml (node: MarkupNode): string { + return this.htmlWriter(node) + } + + wrapBlock (delim: string, firstDelim: string | null, node: MarkupNode, f: () => void): void { + const old = this.delim + this.write(firstDelim ?? delim) + this.delim += delim + f() + this.delim = old + this.closeBlock(node) + } + + atBlank (): boolean { + return /(^|\n)$/.test(this.out) + } + + // :: () + // Ensure the current content ends with a newline. + ensureNewLine (): void { + if (!this.atBlank()) this.out += '\n' + } + + // :: (?string) + // Prepare the state for writing output (closing closed paragraphs, + // adding delimiters, and so on), and then optionally add content + // (unescaped) to the output. + write (content: string): void { + this.flushClose(2) + if (this.delim !== undefined && this.atBlank()) this.out += this.delim + if (content.length > 0) this.out += content + } + + // :: (Node) + // Close the block for the given node. + closeBlock (node: MarkupNode): void { + this.closedNode = node + this.closed = true + } + + // :: (string, ?bool) + // Add the given text to the document. When escape is not `false`, + // it will be escaped. + text (text: string, escape = false): void { + const lines = text.split('\n') + for (let i = 0; i < lines.length; i++) { + const startOfLine = this.atBlank() || this.closed + this.write('') + this.out += escape ? this.esc(lines[i], startOfLine) : lines[i] + if (i !== lines.length - 1) this.out += '\n' + } + } + + // :: (Node) + // Render the given node as a block. + render (node: MarkupNode, parent: MarkupNode, index: number): void { + if (this.nodes[node.type] === undefined) { + throw new Error('Token type `' + node.type + '` not supported by Markdown renderer') + } + this.nodes[node.type](this, node, parent, index) + } + + // :: (Node) + // Render the contents of `parent` as block nodes. + renderContent (parent: MarkupNode): void { + nodeContent(parent).forEach((node: MarkupNode, i: number) => { + this.render(node, parent, i) + }) + } + + reorderMixableMark (state: InlineState, mark: MarkupMark, i: number, len: number): void { + for (let j = 0; j < state.active.length; j++) { + const other = state.active[j] + if (!this.marks[other.type].mixable || this.checkSwitchMarks(i, j, state, mark, other, len)) { + break + } + } + } + + reorderMixableMarks (state: InlineState, len: number): void { + // Try to reorder 'mixable' marks, such as em and strong, which + // in Markdown may be opened and closed in different order, so + // that order of the marks for the token matches the order in + // active. + + for (let i = 0; i < len; i++) { + const mark = state.marks[i] + const mm = this.marks[mark.type] + if (mm == null) { + break + } + if (!mm.mixable) break + this.reorderMixableMark(state, mark, i, len) + } + } + + private checkSwitchMarks ( + i: number, + j: number, + state: InlineState, + mark: MarkupMark, + other: MarkupMark, + len: number + ): boolean { + if (!markEq(mark, other) || i === j) { + return false + } + this.switchMarks(i, j, state, mark, len) + return true + } + + private switchMarks (i: number, j: number, state: InlineState, mark: MarkupMark, len: number): void { + if (i > j) { + state.marks = state.marks + .slice(0, j) + .concat(mark) + .concat(state.marks.slice(j, i)) + .concat(state.marks.slice(i + 1, len)) + } + if (j > i) { + state.marks = state.marks + .slice(0, i) + .concat(state.marks.slice(i + 1, j)) + .concat(mark) + .concat(state.marks.slice(j, len)) + } + } + + renderNodeInline (state: InlineState, index: number): void { + state.marks = state.node?.marks ?? [] + this.updateHardBreakMarks(state, index) + + const leading = this.adjustLeading(state) + + const inner: MarkupMark | undefined = state.marks.length > 0 ? state.marks[state.marks.length - 1] : undefined + const noEsc = inner !== undefined && !(this.marks[inner.type]?.escape ?? false) + const len = state.marks.length - (noEsc ? 1 : 0) + + this.reorderMixableMarks(state, len) + + // Find the prefix of the mark set that didn't change + this.checkCloseMarks(state, len, index) + + // Output any previously expelled trailing whitespace outside the marks + if (leading !== '') this.text(leading) + + // Open the marks that need to be opened + this.checkOpenMarks(state, len, index, inner, noEsc) + } + + private checkOpenMarks ( + state: InlineState, + len: number, + index: number, + inner: MarkupMark | undefined, + noEsc: boolean + ): void { + if (state.node !== undefined) { + this.updateActiveMarks(state, len, index) + + // Render the node. Special case code marks, since their content + // may not be escaped. + if (this.isNoEscapeRequire(state.node, inner, noEsc, state)) { + this.renderMarkText(inner as MarkupMark, state, index) + } else { + this.render(state.node, state.parent, index) + } + } + } + + private isNoEscapeRequire ( + node: MarkupNode, + inner: MarkupMark | undefined, + noEsc: boolean, + state: InlineState + ): boolean { + return inner !== undefined && noEsc && node.type === MarkupNodeType.text + } + + private renderMarkText (inner: MarkupMark, state: InlineState, index: number): void { + this.text( + this.markString(inner, true, state.parent, index) + + (state.node?.text as string) + + this.markString(inner, false, state.parent, index + 1), + false + ) + } + + private updateActiveMarks (state: InlineState, len: number, index: number): void { + while (state.active.length < len) { + const add = state.marks[state.active.length] + state.active.push(add) + this.text(this.markString(add, true, state.parent, index), false) + } + } + + private checkCloseMarks (state: InlineState, len: number, index: number): void { + let keep = 0 + while (keep < Math.min(state.active.length, len) && markEq(state.marks[keep], state.active[keep])) { + ++keep + } + + // Close the marks that need to be closed + while (keep < state.active.length) { + const mark = state.active.pop() + if (mark !== undefined) { + this.text(this.markString(mark, false, state.parent, index), false) + } + } + } + + private adjustLeading (state: InlineState): string { + let leading = state.trailing + state.trailing = '' + // If whitespace has to be expelled from the node, adjust + // leading and trailing accordingly. + const node = state?.node + if (this.isText(node) && this.isMarksHasExpelEnclosingWhitespace(state)) { + const match = /^(\s*)(.*?)(\s*)$/m.exec(node?.text ?? '') + if (match !== null) { + const [leadMatch, innerMatch, trailMatch] = [match[1], match[2], match[3]] + leading += leadMatch + state.trailing = trailMatch + this.adjustLeadingTextNode(leadMatch, trailMatch, state, innerMatch, node as MarkupNode) + } + } + return leading + } + + private isMarksHasExpelEnclosingWhitespace (state: InlineState): boolean { + return state.marks.some((mark) => this.marks[mark.type]?.expelEnclosingWhitespace) + } + + private adjustLeadingTextNode ( + lead: string, + trail: string, + state: InlineState, + inner: string, + node: MarkupNode + ): void { + if (lead !== '' || trail !== '') { + state.node = inner !== undefined ? { ...node, text: inner } : undefined + if (state.node === undefined) { + state.marks = state.active + } + } + } + + private updateHardBreakMarks (state: InlineState, index: number): void { + if (state.node !== undefined && state.node.type === MarkupNodeType.hard_break) { + state.marks = this.filterHardBreakMarks(state.marks, index, state) + } + } + + private filterHardBreakMarks (marks: MarkupMark[], index: number, state: InlineState): MarkupMark[] { + const content = state.parent.content ?? [] + const next = content[index + 1] + if (!this.isHardbreakText(next)) { + return [] + } + return marks.filter((m) => isInSet(m, next.marks ?? [])) + } + + private isHardbreakText (next?: MarkupNode): boolean { + return ( + next !== undefined && (next.type !== MarkupNodeType.text || (next.text !== undefined && /\S/.test(next.text))) + ) + } + + private isText (node?: MarkupNode): boolean { + return node !== undefined && node.type === MarkupNodeType.text && node.text !== undefined + } + + // :: (Node) + // Render the contents of `parent` as inline content. + renderInline (parent: MarkupNode): void { + const state: InlineState = { active: [], trailing: '', parent, marks: [] } + nodeContent(parent).forEach((nde, index) => { + state.node = nde + this.renderNodeInline(state, index) + }) + state.node = undefined + this.renderNodeInline(state, 0) + } + + // :: (Node, string, (number) → string) + // Render a node's content as a list. `delim` should be the extra + // indentation added to all lines except the first in an item, + // `firstDelim` is a function going from an item index to a + // delimiter for the first line of the item. + renderList (node: MarkupNode, delim: string, firstDelim: FirstDelim): void { + this.flushListClose(node) + + const isTight: boolean = + typeof node.attrs?.tight !== 'undefined' ? node.attrs.tight === 'true' : this.options.tightLists + const prevTight = this.inTightList + this.inTightList = isTight + + nodeContent(node).forEach((child, i) => { + this.renderListItem(node, child, i, isTight, delim, firstDelim) + }) + this.inTightList = prevTight + } + + renderListItem ( + node: MarkupNode, + child: MarkupNode, + i: number, + isTight: boolean, + delim: string, + firstDelim: FirstDelim + ): void { + if (i > 0 && isTight) this.flushClose(1) + this.wrapBlock(delim, firstDelim(i, node.content?.[i].attrs, node.attrs), node, () => { + this.render(child, node, i) + }) + } + + private flushListClose (node: MarkupNode): void { + if (this.closed && this.closedNode?.type === node.type) { + this.flushClose(3) + } else if (this.inTightList) { + this.flushClose(1) + } + } + + // :: (string, ?bool) → string + // Escape the given string so that it can safely appear in Markdown + // content. If `startOfLine` is true, also escape characters that + // has special meaning only at the start of the line. + esc (str: string, startOfLine = false): string { + if (str == null) { + return '' + } + str = str.replace(/[`*\\~\[\]]/g, '\\$&') // eslint-disable-line + if (startOfLine) { + str = str.replace(/^[:#\-*+]/, '\\$&').replace(/^(\d+)\./, '$1\\.') + } + str = str.replace(/\r?\n/g, '\\\n') + return str + } + + htmlEsc (str: string): string { + if (str == null) { + return '' + } + + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + } + + quote (str: string): string { + const wrap = !(str?.includes('"') ?? false) ? '""' : !(str?.includes("'") ?? false) ? "''" : '()' + return wrap[0] + str + wrap[1] + } + + // :: (string, number) → string + // Repeat the given string `n` times. + repeat (str: string, n: number): string { + let out = '' + for (let i = 0; i < n; i++) out += str + return out + } + + // : (Mark, bool, string?) → string + // Get the markdown string for a given opening or closing mark. + markString (mark: MarkupMark, open: boolean, parent: MarkupNode, index: number): string { + let value = mark.attrs?.marker + if (value === undefined) { + const info = this.marks[mark.type] + if (info == null) { + throw new Error(`No info for mark ${mark.type}`) + } + value = open ? info.open : info.close + } + return typeof value === 'string' ? value : (value(this, mark, parent, index) ?? '') + } +} + +function makeQuery (obj: Record): string { + return Object.keys(obj) + .filter((it) => it[1] != null) + .map(function (k) { + return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k] as string | number | boolean) + }) + .join('&') +} diff --git a/foundations/core/packages/text-markdown/tsconfig.json b/foundations/core/packages/text-markdown/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/text-markdown/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/text-ydoc/.eslintrc.js b/foundations/core/packages/text-ydoc/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/text-ydoc/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/text-ydoc/CHANGELOG.json b/foundations/core/packages/text-ydoc/CHANGELOG.json new file mode 100644 index 0000000000..4fd6618ea4 --- /dev/null +++ b/foundations/core/packages/text-ydoc/CHANGELOG.json @@ -0,0 +1,92 @@ +{ + "name": "@hcengineering/text-ydoc", + "entries": [ + { + "version": "0.7.18", + "tag": "@hcengineering/text-ydoc_v0.7.18", + "date": "Mon, 27 Oct 2025 17:09:21 GMT", + "comments": { + "patch": [ + { + "comment": "add support for textColor and textStyle marks" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.17", + "tag": "@hcengineering/text-ydoc_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/text-ydoc_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + }, + { + "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.4` to `0.7.5`" + }, + { + "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/text-ydoc_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.3` to `0.7.4`" + }, + { + "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/text-ydoc_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/text-ydoc/CHANGELOG.md b/foundations/core/packages/text-ydoc/CHANGELOG.md new file mode 100644 index 0000000000..05fe657b1e --- /dev/null +++ b/foundations/core/packages/text-ydoc/CHANGELOG.md @@ -0,0 +1,35 @@ +# Change Log - @hcengineering/text-ydoc + +This log was last generated on Mon, 27 Oct 2025 17:09:21 GMT and should not be manually modified. + +## 0.7.18 +Mon, 27 Oct 2025 17:09:21 GMT + +### Patches + +- add support for textColor and textStyle marks + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/text-ydoc/config/rig.json b/foundations/core/packages/text-ydoc/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/text-ydoc/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/text-ydoc/jest.config.js b/foundations/core/packages/text-ydoc/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/text-ydoc/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/text-ydoc/package.json b/foundations/core/packages/text-ydoc/package.json new file mode 100644 index 0000000000..c279bce5ac --- /dev/null +++ b/foundations/core/packages/text-ydoc/package.json @@ -0,0 +1,63 @@ +{ + "name": "@hcengineering/text-ydoc", + "version": "0.7.18", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "build:watch": "compile", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "jest-environment-jsdom": "^30.2.0", + "fast-equals": "^5.2.2", + "y-prosemirror": "^1.3.7", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/text": "workspace:^0.7.18", + "@hcengineering/text-core": "workspace:^0.7.18", + "yjs": "^13.6.27", + "y-protocols": "^1.0.6" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/text-ydoc/src/__tests__/ydoc.test.ts b/foundations/core/packages/text-ydoc/src/__tests__/ydoc.test.ts new file mode 100644 index 0000000000..43d6f6fab5 --- /dev/null +++ b/foundations/core/packages/text-ydoc/src/__tests__/ydoc.test.ts @@ -0,0 +1,259 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Markup, generateId } from '@hcengineering/core' +import { type MarkupNode, jsonToMarkup, jsonToPmNode, markupToJSON } from '@hcengineering/text' +import { prosemirrorToYXmlFragment, yDocToProsemirrorJSON } from 'y-prosemirror' +import { deepEqual } from 'fast-equals' +import { Doc as YDoc } from 'yjs' +import { markupToYDoc, yDocToMarkup } from '../ydoc' + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toEqualMarkup: (expected: string) => R + toEqualYdoc: (expected: YDoc) => R + } + } +} + +expect.extend({ + toEqualMarkup (received: string, expected: string) { + const pass = received === expected || deepEqual(JSON.parse(received), JSON.parse(expected)) + return { + message: () => + pass + ? `Expected markup strings NOT to be equal:\n Expected: ${expected}\n Received: ${received}` + : `Expected markup strings to be equal:\n Expected: ${expected}\n Received: ${received}`, + pass + } + }, + + toEqualYdoc (received: YDoc, expected: YDoc) { + const expectedJSON = expected.toJSON() + const receivedJSON = received.toJSON() + + const pass = deepEqual(expectedJSON, receivedJSON) + return { + message: () => + pass + ? `Expected yjs documents NOT to be equal:\n Expected: ${JSON.stringify(expectedJSON)}\n Received: ${JSON.stringify(receivedJSON)}` + : `Expected yjs documents to be equal:\n Expected: ${JSON.stringify(expectedJSON)}\n Received: ${JSON.stringify(receivedJSON)}`, + pass + } + } +}) + +function referenceMarkupToYDoc (markup: Markup, field: string): YDoc { + const ydoc = new YDoc({ guid: generateId() }) + const fragment = ydoc.getXmlFragment(field) + prosemirrorToYXmlFragment(jsonToPmNode(markupToJSON(markup)), fragment) + return ydoc +} + +function referenceYDocToMarkup (ydoc: YDoc, field: string): Markup { + const json = yDocToProsemirrorJSON(ydoc, field) + return jsonToMarkup(json as MarkupNode) +} + +const markups: Array<{ name: string, markup: Markup, skipYdocCompare?: boolean }> = [ + { + name: 'text', + markup: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello world"}]}]}' + }, + { + name: 'text with bold mark', + markup: + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"hello world"}]}]}' + }, + { + name: 'separate paragraphs with bold mark', + markup: + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"hello"}]},{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"world"}]}]}' + }, + { + name: 'mixed text and text with bold mark', + markup: + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello "},{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"world"}]}]}' + }, + { + name: 'mixed text with italic and text with bold and italic marks', + markup: + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"hello "},{"type":"text","marks":[{"type":"bold","attrs":{}},{"type":"italic","attrs":{}}],"text":"world"}]}]}' + }, + { + name: 'text with link and italic marks', + markup: + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello "},{"type":"text","text":"hello world","marks":[{"type":"link","attrs":{"href":"http://example.com","target":"_blank","rel":"noopener noreferrer","class":"cursor-pointer"}},{"type":"italic","attrs":{}}]}]}]}' + }, + { + name: 'image', + markup: + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"image","attrs":{"src":"http://example.com/image.jpg","alt":"image"}}]}]}' + }, + { + name: 'table with formatting inside', + markup: + '{"type":"doc","content":[{"type":"table","content":[{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"1"}]}]},{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"2"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"codeBlock","content":[{"type":"text","text":"3"}]}]},{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","text":"4"}]}]}]}]}]}' + }, + { + name: 'non-overlapping marks', + markup: `{ + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "xy", + "marks": [ + { + "type": "inline-comment", + "attrs": { + "thread": "1" + } + } + ] + }, + { + "type": "text", + "text": "z", + "marks": [ + { + "type": "inline-comment", + "attrs": { + "thread": "2" + } + } + ] + } + ] + } + ] +}`, + skipYdocCompare: true + }, + { + name: 'overlapping marks', + markup: `{ + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "x", + "marks": [ + { + "type": "inline-comment", + "attrs": { + "thread": "1" + } + } + ] + }, + { + "type": "text", + "text": "y", + "marks": [ + { + "type": "inline-comment", + "attrs": { + "thread": "1" + } + }, + { + "type": "inline-comment", + "attrs": { + "thread": "2" + } + } + ] + }, + { + "type": "text", + "text": "z", + "marks": [ + { + "type": "inline-comment", + "attrs": { + "thread": "2" + } + } + ] + } + ] + } + ] +}`, + skipYdocCompare: true + } +] + +describe('markupToYDoc', () => { + describe.each(markups)('compare with reference', ({ name, markup, skipYdocCompare }) => { + if (skipYdocCompare !== true) { + it(name, () => { + const expected = referenceMarkupToYDoc(markup, 'test') + const actual = markupToYDoc(markup, 'test') + expect(actual).toEqualYdoc(expected) + }) + } + }) + + describe.each(markups)('converts markup to ydoc and back', ({ name, markup }) => { + it(name, () => { + const ydoc = markupToYDoc(markup, 'test') + const actual = yDocToMarkup(ydoc, 'test') + + expect(actual).toEqualMarkup(markup) + }) + }) +}) + +describe('yDocToMarkup', () => { + describe.each(markups)('compare with original', ({ name, markup }) => { + it(name, () => { + const ydoc = referenceMarkupToYDoc(markup, 'test') + const actual = yDocToMarkup(ydoc, 'test') + + expect(actual).toEqualMarkup(markup) + }) + }) + + describe.each(markups)('compare with reference', ({ name, markup }) => { + it(name, () => { + const ydoc = referenceMarkupToYDoc(markup, 'test') + const expected = referenceYDocToMarkup(ydoc, 'test') + const actual = yDocToMarkup(ydoc, 'test') + + expect(actual).toEqualMarkup(expected) + }) + }) + + describe.each(markups)('converts ydoc to markup and back', ({ name, markup, skipYdocCompare }) => { + if (skipYdocCompare !== true) { + it(name, () => { + const expected = referenceMarkupToYDoc(markup, 'test') + const actual = referenceMarkupToYDoc(yDocToMarkup(expected, 'test'), 'test') + + expect(actual).toEqualYdoc(expected) + }) + } + }) +}) diff --git a/foundations/core/packages/text-ydoc/src/index.ts b/foundations/core/packages/text-ydoc/src/index.ts new file mode 100644 index 0000000000..d6dbacbc50 --- /dev/null +++ b/foundations/core/packages/text-ydoc/src/index.ts @@ -0,0 +1,16 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './ydoc' diff --git a/foundations/core/packages/text-ydoc/src/ydoc.ts b/foundations/core/packages/text-ydoc/src/ydoc.ts new file mode 100644 index 0000000000..f777c7eb4a --- /dev/null +++ b/foundations/core/packages/text-ydoc/src/ydoc.ts @@ -0,0 +1,142 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { generateId, Markup } from '@hcengineering/core' +import { jsonToMarkup, MarkupMarkType, MarkupNodeType, markupToJSON, type MarkupNode } from '@hcengineering/text' +import { hashAttrs, stripHash } from '@hcengineering/text-core' +import { Doc as YDoc, XmlElement as YXmlElement, XmlFragment as YXmlFragment, XmlText as YXmlText } from 'yjs' + +/** + * Convert Markup to Y.Doc + * + * @public + */ +export function markupToYDoc (markup: Markup, field: string): YDoc { + return jsonToYDoc(markupToJSON(markup), field) +} + +/** + * Convert Markup JSON to Y.Doc + * + * @public + */ +export function jsonToYDoc (json: MarkupNode, field: string): YDoc { + const ydoc = new YDoc({ guid: generateId() }) + const fragment = ydoc.getXmlFragment(field) + + const nodes = json.type === 'doc' ? (json.content ?? []) : [json] + nodes.map((p) => nodeToXmlElement(fragment, p)) + + return ydoc +} + +function nodeToXmlElement (parent: YXmlFragment, node: MarkupNode): YXmlElement | YXmlText { + const elem = node.type === 'text' ? new YXmlText() : new YXmlElement(node.type) + parent.push([elem]) + + if (elem instanceof YXmlElement) { + if (node.content !== undefined && node.content.length > 0) { + node.content.map((p) => nodeToXmlElement(elem, p)) + } + } else if (elem instanceof YXmlText) { + // https://github.com/yjs/y-prosemirror/blob/master/src/plugins/sync-plugin.js#L777 + const attributes: Record = {} + if (node.marks !== undefined) { + const attrCount = new Map() + node.marks.forEach((mark) => { + attrCount.set(mark.type, (attrCount.get(mark.type) ?? 0) + 1) + }) + + node.marks.forEach((mark) => { + const count = attrCount.get(mark.type) ?? 0 + const type = count > 1 ? `${mark.type}--${hashAttrs(mark.attrs)}` : mark.type + attributes[type] = mark.attrs ?? {} + }) + } + + elem.applyDelta([ + { + insert: node.text ?? '', + attributes + } + ]) + } + + if (node.attrs !== undefined) { + Object.entries(node.attrs).forEach(([key, value]) => { + elem.setAttribute(key, value) + }) + } + + return elem +} + +/** + * Convert Y.Doc to Markup + * + * @public + */ +export function yDocToMarkup (ydoc: YDoc, field: string): Markup { + const fragment = ydoc.getXmlFragment(field) + const json = xmlFragmentToNode(fragment) + return jsonToMarkup({ type: MarkupNodeType.doc, content: json }) +} + +function xmlFragmentToNode (fragment: YXmlFragment): MarkupNode[] { + const result: MarkupNode[] = [] + + for (let i = 0; i < fragment.length; i++) { + const item = fragment.get(i) + if (item instanceof YXmlElement) { + const node: MarkupNode = { + type: item.nodeName as MarkupNodeType + } + + // Handle attributes + const attrs = item.getAttributes() + if (Object.keys(attrs).length > 0) { + node.attrs = attrs + } + + // Handle content + if (item.length > 0) { + node.content = xmlFragmentToNode(item) + } + + result.push(node) + } else if (item instanceof YXmlText) { + // Handle text with marks + const delta = item.toDelta() + for (const op of delta) { + const textNode: MarkupNode = { + type: MarkupNodeType.text, + text: op.insert + } + + // Convert attributes to marks + if (op.attributes != null) { + textNode.marks = Object.entries(op.attributes).map(([type, attrs]) => ({ + type: stripHash(type) as MarkupMarkType, + attrs: attrs as Record + })) + } + + result.push(textNode) + } + } + } + + return result +} diff --git a/foundations/core/packages/text-ydoc/tsconfig.json b/foundations/core/packages/text-ydoc/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/text-ydoc/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/text/.eslintrc.js b/foundations/core/packages/text/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/text/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/text/CHANGELOG.json b/foundations/core/packages/text/CHANGELOG.json new file mode 100644 index 0000000000..26a36bbca7 --- /dev/null +++ b/foundations/core/packages/text/CHANGELOG.json @@ -0,0 +1,81 @@ +{ + "name": "@hcengineering/text", + "entries": [ + { + "version": "0.7.18", + "tag": "@hcengineering/text_v0.7.18", + "date": "Mon, 27 Oct 2025 17:09:21 GMT", + "comments": { + "patch": [ + { + "comment": "add support for textColor and textStyle marks" + } + ] + } + }, + { + "version": "0.7.17", + "tag": "@hcengineering/text_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/text_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + }, + { + "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/text_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/text_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/text/CHANGELOG.md b/foundations/core/packages/text/CHANGELOG.md new file mode 100644 index 0000000000..e0442c313d --- /dev/null +++ b/foundations/core/packages/text/CHANGELOG.md @@ -0,0 +1,35 @@ +# Change Log - @hcengineering/text + +This log was last generated on Mon, 27 Oct 2025 17:09:21 GMT and should not be manually modified. + +## 0.7.18 +Mon, 27 Oct 2025 17:09:21 GMT + +### Patches + +- add support for textColor and textStyle marks + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/text/config/rig.json b/foundations/core/packages/text/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/text/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/text/jest.config.js b/foundations/core/packages/text/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/text/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/text/package.json b/foundations/core/packages/text/package.json new file mode 100644 index 0000000000..83adb46dc4 --- /dev/null +++ b/foundations/core/packages/text/package.json @@ -0,0 +1,96 @@ +{ + "name": "@hcengineering/text", + "version": "0.7.18", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent --coverage", + "build:watch": "compile", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/markdown-it": "~13.0.0", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/text-core": "workspace:^0.7.18", + "@tiptap/core": "^2.11.7", + "@tiptap/html": "^2.11.7", + "@tiptap/pm": "^2.11.7", + "@tiptap/starter-kit": "^2.11.7", + "@tiptap/extension-gapcursor": "^2.11.7", + "@tiptap/extension-heading": "^2.11.7", + "@tiptap/extension-highlight": "^2.11.7", + "@tiptap/extension-history": "^2.11.7", + "@tiptap/extension-link": "^2.11.7", + "@tiptap/extension-mention": "^2.11.7", + "@tiptap/extension-table": "^2.11.7", + "@tiptap/extension-table-cell": "^2.11.7", + "@tiptap/extension-table-header": "^2.11.7", + "@tiptap/extension-table-row": "^2.11.7", + "@tiptap/extension-task-item": "^2.11.7", + "@tiptap/extension-task-list": "^2.11.7", + "@tiptap/extension-bold": "^2.11.7", + "@tiptap/extension-blockquote": "^2.11.7", + "@tiptap/extension-text": "^2.11.7", + "@tiptap/extension-document": "^2.11.7", + "@tiptap/extension-ordered-list": "^2.11.7", + "@tiptap/extension-bullet-list": "^2.11.7", + "@tiptap/extension-list-item": "^2.11.7", + "@tiptap/extension-dropcursor": "^2.11.7", + "@tiptap/extension-hard-break": "^2.11.7", + "@tiptap/extension-horizontal-rule": "^2.11.7", + "@tiptap/extension-italic": "^2.11.7", + "@tiptap/extension-paragraph": "^2.11.7", + "@tiptap/extension-strike": "^2.11.7", + "@tiptap/extension-typography": "^2.11.7", + "@tiptap/extension-code-block": "^2.11.7", + "@tiptap/extension-code": "^2.11.7", + "@tiptap/extension-underline": "^2.11.7", + "@tiptap/suggestion": "^2.11.7", + "prosemirror-codemark": "^0.4.2", + "fast-equals": "^5.2.2", + "@tiptap/extension-text-align": "~2.11.0", + "@tiptap/extension-text-style": "~2.11.0" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/text/src/extensions.ts b/foundations/core/packages/text/src/extensions.ts new file mode 100644 index 0000000000..01e3f270b7 --- /dev/null +++ b/foundations/core/packages/text/src/extensions.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { ServerKit } from './kits/server-kit' + +export const defaultExtensions = [ServerKit] diff --git a/foundations/core/packages/text/src/index.ts b/foundations/core/packages/text/src/index.ts new file mode 100644 index 0000000000..ae3ab5ea13 --- /dev/null +++ b/foundations/core/packages/text/src/index.ts @@ -0,0 +1,29 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './extensions' +export * from '@hcengineering/text-core' +export * from './nodes' +export * from './markup/utils' +export * from './marks/code' +export * from './marks/colors' +export * from './marks/noteBase' +export * from './marks/inlineComment' + +export * from './kits/common-kit' +export * from './kits/server-kit' +export * from './kit' + +export * from './tiptapExtensions' diff --git a/foundations/core/packages/text/src/kit.ts b/foundations/core/packages/text/src/kit.ts new file mode 100644 index 0000000000..7600dbf98f --- /dev/null +++ b/foundations/core/packages/text/src/kit.ts @@ -0,0 +1,89 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { AnyExtension, Extension } from '@tiptap/core' + +export type ExtensionFactory = ( + extension: T, + options?: Partial | boolean +) => ExtensionSpec + +export interface ExtensionSpec { + extension: T + options: Partial | boolean +} + +export type ExtensionSpecOptions = { + [K in keyof T]: T[K] extends ExtensionSpec ? Partial | boolean : never +} + +export function extensionKit ( + name: string, + fn: (e: ExtensionFactory, o: O) => K +): Extension> { + return Extension.create({ + name, + addExtensions () { + const e: ExtensionFactory = (extension, options) => { + // ExtensionFactory -> ExtensionSpec is mostly intented as a wrapper to provide a comfortable typing + return { extension, options: options ?? true } + } + + const extensions: AnyExtension[] = [] + const entries: object = fn(e, this.options) as any + + for (const [key, _data] of Object.entries(entries)) { + const data = _data as ExtensionSpec + if (data?.extension === undefined) continue + + let options = mergeKitOptions(data.options, (this.options as any)[key]) ?? false + + // "false" is indication that the extension should not be loaded at all + if (options === false) continue + + // "true" is indication that the extension should be loaded with whatever options some parent loader provided + if (options === true) { + if (typeof data.options === 'object') { + options = data.options + } else { + options = {} + } + } + + extensions.push(data.extension.configure(options)) + } + + return extensions + } + }) +} + +export function mergeKitOptions> (target: T, source: T): T { + if (typeof target === 'object' && typeof source === 'object') { + const output = { ...target } + Object.keys(source).forEach((key: keyof T) => { + const a = target[key] + const b = source[key] + if (typeof a === 'object' && typeof b === 'object') { + output[key] = mergeKitOptions(a, b) + } else { + output[key] = b ?? a + } + }) + return output + } + + return source ?? target +} diff --git a/foundations/core/packages/text/src/kits/common-kit.ts b/foundations/core/packages/text/src/kits/common-kit.ts new file mode 100644 index 0000000000..4e4639a43e --- /dev/null +++ b/foundations/core/packages/text/src/kits/common-kit.ts @@ -0,0 +1,135 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + Blockquote, + Bold, + BulletList, + Document, + Dropcursor, + Gapcursor, + Heading, + History, + HorizontalRule, + Italic, + Link, + ListItem, + OrderedList, + Paragraph, + Strike, + Table, + TableCell, + TableHeader, + TableRow, + Text, + TextAlign, + TextStyle, + Typography, + Underline +} from '../tiptapExtensions' + +import { ExtensionFactory, extensionKit } from '../kit' +import { BackgroundColor, TextColor } from '../marks/colors' +import { InlineCommentMark } from '../marks/inlineComment' +import { CodeBlockExtension, codeBlockOptions, CommentNode, MarkdownNode, TodoItemNode, TodoListNode } from '../nodes' + +import { CodeExtension, codeOptions } from '../marks/code' +import { MermaidExtension, mermaidOptions } from '../nodes/mermaid' + +export const CommonKitFactory = (e: ExtensionFactory) => + ({ + text: e(Text), + document: e(Document), + paragraph: e(Paragraph), + + bold: e(Bold), + italic: e(Italic), + strike: e(Strike), + + comment: e(CommentNode), + markdown: e(MarkdownNode, { HTMLAttributes: { class: 'proseCodeBlock' } }), + + inlineComment: e(InlineCommentMark), + + horizontalRule: e(HorizontalRule), + heading: e(Heading), + underline: e(Underline), + blockquote: e(Blockquote, { HTMLAttributes: { class: 'proseBlockQuote' } }), + link: e(Link.extend({ inclusive: false }), { + openOnClick: false, + HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' } + }), + textAlign: e(TextAlign, { + types: ['heading', 'paragraph'], + alignments: ['left', 'center', 'right'], + defaultAlignment: null + }), + + typography: e(Typography), + + dropcursor: e(Dropcursor), + gapcursor: e(Gapcursor), + history: e(History) + }) as const + +export const CommonKit = extensionKit('common-kit', CommonKitFactory) + +export const TextColorStylingKit = extensionKit( + 'text-color-styling', + (e) => + ({ + textStyle: e(TextStyle), + testColor: e(TextColor), + backgroundColor: e(BackgroundColor, { types: ['tableCell'] }) + }) as const +) + +export const TableKit = extensionKit( + 'table-kit', + (e) => + ({ + table: e(Table, { resizable: false, HTMLAttributes: { class: 'proseTable' } }), + tableRow: e(TableRow), + tableHeader: e(TableHeader), + tableCell: e(TableCell) + }) as const +) + +export const CodeSnippetsKit = extensionKit( + 'code-snippet-kit', + (e) => + ({ + codeBlock: e(CodeBlockExtension, codeBlockOptions), + codeBlockMermaid: e(MermaidExtension, mermaidOptions), + codeInline: e(CodeExtension, codeOptions) + }) as const +) + +export const CommonListKitFactory = (e: ExtensionFactory) => + ({ + listItem: e(ListItem.extend({ group: 'listItems' })), + bulletList: e(BulletList.extend({ content: 'listItems+' })), + orderedList: e(OrderedList.extend({ content: 'listItems+' })) + }) as const + +export const ListKit = extensionKit( + 'list-kit', + (e) => + ({ + ...CommonListKitFactory(e), + todoItem: e(TodoItemNode), + todoList: e(TodoListNode) + }) as const +) diff --git a/foundations/core/packages/text/src/kits/server-kit.ts b/foundations/core/packages/text/src/kits/server-kit.ts new file mode 100644 index 0000000000..3778c22fd0 --- /dev/null +++ b/foundations/core/packages/text/src/kits/server-kit.ts @@ -0,0 +1,55 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { NoteBaseExtension } from '../marks/noteBase' +import { QMSInlineCommentMark } from '../marks/qmsInlineCommentMark' + +import { EmojiNode } from '../nodes/emoji' +import { FileNode } from '../nodes/file' +import { ImageNode } from '../nodes/image' +import { ReferenceNode } from '../nodes/reference' + +import { EmbedNode } from '../nodes/embed' + +import { ExtensionFactory, extensionKit } from '../kit' +import { HardBreak } from '../tiptapExtensions' +import { CodeSnippetsKit, CommonKitFactory, ListKit, TableKit, TextColorStylingKit } from './common-kit' + +export const ServerKitFactory = (e: ExtensionFactory) => + ({ + ...CommonKitFactory(e), + + // ========================================================================================== + // Extensions and kits with separate / extended implementations in the client-side editor kit + // See file://./../../../../plugins/text-editor-resources/src/kits/editor-kit.ts + // ============================================================================= + + lists: e(ListKit), + codeSnippets: e(CodeSnippetsKit), + tables: e(TableKit), + textColorStyling: e(TextColorStylingKit), + + hardBreak: e(HardBreak), + reference: e(ReferenceNode), + file: e(FileNode), + image: e(ImageNode), + embed: e(EmbedNode), + emoji: e(EmojiNode), + + inlineNote: e(NoteBaseExtension), // Semi-deprecated, should be removed in the future + qmsInlineCommentMark: e(QMSInlineCommentMark) // Semi-deprecated, should be removed in the future + }) as const + +export const ServerKit = extensionKit('server-kit', ServerKitFactory) diff --git a/foundations/core/packages/text/src/marks/code.ts b/foundations/core/packages/text/src/marks/code.ts new file mode 100644 index 0000000000..282848acfe --- /dev/null +++ b/foundations/core/packages/text/src/marks/code.ts @@ -0,0 +1,82 @@ +import Code, { CodeOptions } from '@tiptap/extension-code' +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { EditorView } from '@tiptap/pm/view' +import { Slice } from '@tiptap/pm/model' +import codemark from 'prosemirror-codemark' + +export const codeOptions: CodeOptions = { + HTMLAttributes: { + class: 'proseCode' + } +} + +/** + * Note: do not forget to include css import for UI use of this extension. + * import 'prosemirror-codemark/dist/codemark.css' + */ +export const CodeExtension = Code.extend({ + addProseMirrorPlugins () { + return [ + ...codemark({ markType: this.editor.schema.marks.code }), + new Plugin({ + key: new PluginKey('code-consecutive-backticks'), + props: { + // Typing a character inside of two backticks will wrap the character + // in an inline code mark. + handleTextInput: (view: EditorView, from: number, to: number, text: string) => { + const { state } = view + + // Prevent access out of document bounds + if (from === 0 || to === state.doc.nodeSize - 1 || text === '`') { + return false + } + + if ( + from === to && + state.doc.textBetween(from - 1, from) === '`' && + state.doc.textBetween(to, to + 1) === '`' + ) { + const start = from - 1 + const end = to + 1 + view.dispatch( + state.tr + .delete(start, end) + .insertText(text, start) + .addMark(start, start + text.length, state.schema.marks.code.create()) + ) + + return true + } + + return false + }, + + // Pasting a character inside of two backticks will wrap the character + // in an inline code mark. + handlePaste: (view: EditorView, _event: Event, slice: Slice) => { + const { state } = view + const { from, to } = state.selection + + // Prevent access out of document bounds + if (from === 0 || to === state.doc.nodeSize - 1) { + return false + } + + const start = from - 1 + const end = to + 1 + if (from === to && state.doc.textBetween(start, from) === '`' && state.doc.textBetween(to, end) === '`') { + view.dispatch( + state.tr + .replaceRange(start, end, slice) + .addMark(start, start + slice.size, state.schema.marks.code.create()) + ) + return true + } + + return false + } + } + }) + ] + } +}) diff --git a/foundations/core/packages/text/src/marks/colors.ts b/foundations/core/packages/text/src/marks/colors.ts new file mode 100644 index 0000000000..a29f83a49f --- /dev/null +++ b/foundations/core/packages/text/src/marks/colors.ts @@ -0,0 +1,142 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Extension } from '@tiptap/core' +import '@tiptap/extension-text-style' + +export interface BackgroundColorOptions { + types: string[] +} + +declare module '@tiptap/core' { + interface Commands { + colors: { + setTextColor: (color: string) => ReturnType + unsetTextColor: () => ReturnType + setBackgroundColor: (color: string) => ReturnType + unsetBackgroundColor: () => ReturnType + } + } +} + +export const BackgroundColor = Extension.create({ + name: 'backgroundColor', + + addOptions () { + return { + types: [] + } + }, + + addGlobalAttributes () { + return [ + { + types: this.options.types, + attributes: { + backgroundColor: { + parseHTML: (element) => { + return element.getAttribute('data-background-color') ?? undefined + }, + renderHTML: (attributes) => { + if (typeof attributes.backgroundColor !== 'string') { + return {} + } + + return { + 'data-background-color': attributes.backgroundColor, + style: `background-color: ${attributes.backgroundColor}` + } + } + } + } + } + ] + }, + + addCommands () { + return { + setBackgroundColor: + (backgroundColor: string) => + ({ commands }) => { + return this.options.types + .map((type) => commands.updateAttributes(type, { backgroundColor })) + .every((response) => response) + }, + + unsetBackgroundColor: + () => + ({ commands }) => { + return this.options.types + .map((type) => commands.resetAttributes(type, 'backgroundColor')) + .every((response) => response) + } + } + } +}) + +export interface TextColorOptions { + types: string[] +} + +export const TextColor = Extension.create({ + name: 'textColor', + + addOptions () { + return { + types: ['textStyle'] + } + }, + + addGlobalAttributes () { + return [ + { + types: this.options.types, + attributes: { + color: { + parseHTML: (element) => { + return element.getAttribute('data-color') ?? undefined + }, + renderHTML: (attributes) => { + if (typeof attributes.color !== 'string') { + return {} + } + + return { + 'data-color': attributes.color, + style: `color: ${attributes.color}` + } + } + } + } + } + ] + }, + + addCommands () { + return { + setTextColor: + (color: string) => + ({ chain }) => { + return chain().setMark('textStyle', { color }).run() + }, + + unsetTextColor: + () => + ({ chain }) => { + return chain().unsetMark('textStyle').run() + } + } + } +}) diff --git a/foundations/core/packages/text/src/marks/inlineComment.ts b/foundations/core/packages/text/src/marks/inlineComment.ts new file mode 100644 index 0000000000..a2d942fc57 --- /dev/null +++ b/foundations/core/packages/text/src/marks/inlineComment.ts @@ -0,0 +1,87 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Mark } from '@tiptap/core' +import { Fragment, Node, Slice } from '@tiptap/pm/model' +import { Plugin, PluginKey } from '@tiptap/pm/state' + +export const InlineCommentMark = Mark.create({ + name: 'inline-comment', + excludes: '', + + inclusive: false, + + parseHTML () { + return [ + { + tag: 'span.proseInlineComment[data-inline-comment-thread]' + } + ] + }, + + renderHTML ({ HTMLAttributes, mark }) { + return ['span', { ...HTMLAttributes, class: 'proseInlineComment' }, 0] + }, + + addAttributes () { + const name = 'data-inline-comment-thread-id' + return { + thread: { + default: undefined, + parseHTML: (element) => { + return element.getAttribute(name) + }, + renderHTML: (attributes) => { + return { [name]: attributes.thread } + } + } + } + }, + + addProseMirrorPlugins () { + return [...(this.parent?.() ?? []), InlineCommentPasteFixPlugin()] + } +}) + +function removeMarkFromNode (node: Node, name: string): Node { + if (node.isText) { + return node.mark(node.marks.filter((mark) => mark.type.name !== name)) + } + + if (node.content.size > 0) { + const nodes: Node[] = [] + node.content.forEach((child) => { + nodes.push(removeMarkFromNode(child, name)) + }) + return node.copy(Fragment.fromArray(nodes)) + } + + return node +} + +export function InlineCommentPasteFixPlugin (): Plugin { + return new Plugin({ + key: new PluginKey('inline-comment-paste-fix-plugin'), + props: { + transformPasted: (slice) => { + const nodes: Node[] = [] + slice.content.forEach((node) => { + nodes.push(removeMarkFromNode(node, 'inline-comment')) + }) + return new Slice(Fragment.fromArray(nodes), slice.openStart, slice.openEnd) + } + } + }) +} diff --git a/foundations/core/packages/text/src/marks/noteBase.ts b/foundations/core/packages/text/src/marks/noteBase.ts new file mode 100644 index 0000000000..1114d061ce --- /dev/null +++ b/foundations/core/packages/text/src/marks/noteBase.ts @@ -0,0 +1,53 @@ +import { Mark } from '@tiptap/core' +import { getDataAttribute } from '../nodes' + +export const name = 'note' +export enum NoteKind { + Neutral = 'neutral', + Dangerous = 'dangerous', + DangerousLight = 'dangerous-light', + Warning = 'warning', + WarningLight = 'warning-light', + Positive = 'positive', + PositiveLight = 'positive-light', + Primary = 'primary', + PrimaryLight = 'primary-light' +} + +declare module '@tiptap/core' { + export interface Commands { + [name]: { + setNote: (text: string, kind: NoteKind) => ReturnType + unsetNote: () => ReturnType + } + } +} + +export const NoteBaseExtension = Mark.create({ + name, + + parseHTML () { + return [ + { + tag: `span[data-mark="${name}"]` + } + ] + }, + + renderHTML ({ HTMLAttributes, mark }) { + return [ + 'span', + { ...HTMLAttributes, 'data-mark': this.name, class: `theme-text-editor-note-anchor ${mark.attrs.kind}` }, + 0 + ] + }, + + addAttributes () { + return { + title: { + default: null + }, + kind: getDataAttribute('kind', { default: NoteKind.Neutral }) + } + } +}) diff --git a/foundations/core/packages/text/src/marks/qmsInlineCommentMark.ts b/foundations/core/packages/text/src/marks/qmsInlineCommentMark.ts new file mode 100644 index 0000000000..72391e10dc --- /dev/null +++ b/foundations/core/packages/text/src/marks/qmsInlineCommentMark.ts @@ -0,0 +1,23 @@ +import { Mark } from '@tiptap/core' + +const NAME = 'node-uuid' + +/** + * @public + */ +export const QMSInlineCommentMark = Mark.create({ + name: NAME, + inline: true, + + parseHTML () { + return [ + { + tag: `span[${NAME}]` + } + ] + }, + + renderHTML ({ HTMLAttributes }) { + return ['span', HTMLAttributes, 0] + } +}) diff --git a/foundations/core/packages/text/src/markup/__tests__/dsl.test.ts b/foundations/core/packages/text/src/markup/__tests__/dsl.test.ts new file mode 100644 index 0000000000..37841dfc8f --- /dev/null +++ b/foundations/core/packages/text/src/markup/__tests__/dsl.test.ts @@ -0,0 +1,106 @@ +import { + markLink, + markUnderline, + MarkupNodeType, + nodeDoc, + nodeImage, + nodeParagraph, + nodeReference, + nodeText +} from '@hcengineering/text-core' +import { jsonToHTML } from '../utils' + +describe('dsl', () => { + it('returns a MarkupNode for complex doc', () => { + const doc = nodeDoc( + nodeParagraph(nodeText('Hello, '), nodeReference({ id: '123', label: 'World', objectclass: 'world' })), + nodeParagraph( + nodeText('Check out '), + markLink({ href: 'https://example.com', title: 'this link' }, markUnderline(nodeText('this link'))), + nodeText('.') + ) + ) + expect(jsonToHTML(doc)).toEqual( + '

Hello, @World

Check out this link.

' + ) + }) +}) + +describe('nodeDoc', () => { + it('returns a MarkupNode with type "doc"', () => { + const result = nodeDoc() + expect(result.type).toEqual(MarkupNodeType.doc) + }) + + it('returns a MarkupNode with the provided content', () => { + const content = [ + { type: MarkupNodeType.paragraph, content: [{ type: MarkupNodeType.text, text: 'Hello' }] }, + { type: MarkupNodeType.paragraph, content: [{ type: MarkupNodeType.text, text: 'World' }] } + ] + const result = nodeDoc(...content) + expect(result.content).toEqual(content) + }) + + it('returns an empty MarkupNode if no content is provided', () => { + const result = nodeDoc() + expect(result.content).toEqual([]) + }) +}) + +describe('nodeParagraph', () => { + it('returns a MarkupNode with type "paragraph"', () => { + const result = nodeParagraph() + expect(result.type).toEqual(MarkupNodeType.paragraph) + }) + + it('returns a MarkupNode with the provided content', () => { + const content = [{ type: MarkupNodeType.text, text: 'Hello' }] + const result = nodeParagraph(...content) + expect(result.content).toEqual(content) + }) + + it('returns an empty MarkupNode if no content is provided', () => { + const result = nodeParagraph() + expect(result.content).toEqual([]) + }) +}) + +describe('nodeText', () => { + it('returns a MarkupNode with type "text"', () => { + const result = nodeText('Hello') + expect(result.type).toEqual(MarkupNodeType.text) + }) + + it('returns a MarkupNode with the provided text', () => { + const result = nodeText('Hello') + expect(result.text).toEqual('Hello') + }) +}) + +describe('nodeImage', () => { + it('returns a MarkupNode with type "image"', () => { + const attrs = { src: 'image.jpg' } + const result = nodeImage(attrs) + expect(result.type).toEqual(MarkupNodeType.image) + }) + + it('returns a MarkupNode with the provided attributes', () => { + const attrs = { src: 'image.jpg', alt: 'Image', width: 500, height: 300 } + const result = nodeImage(attrs) + expect(result.attrs).toEqual(attrs) + }) +}) + +describe('nodeReference', () => { + it('returns a MarkupNode with type "reference"', () => { + const attrs = { id: '123', label: 'Reference', objectclass: 'class' } + const result = nodeReference(attrs) + expect(result.type).toEqual(MarkupNodeType.reference) + }) + + it('returns a MarkupNode with the provided attributes', () => { + const attrs = { id: '123', label: 'Reference', objectclass: 'class' } + const result = nodeReference(attrs) + expect(result.attrs).toEqual(attrs) + }) +}) diff --git a/foundations/core/packages/text/src/markup/__tests__/utils.test.ts b/foundations/core/packages/text/src/markup/__tests__/utils.test.ts new file mode 100644 index 0000000000..2bff4dc275 --- /dev/null +++ b/foundations/core/packages/text/src/markup/__tests__/utils.test.ts @@ -0,0 +1,416 @@ +/** + * @jest-environment jsdom + */ + +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + areEqualMarkups, + isEmptyMarkup, + isEmptyNode, + jsonToMarkup, + MarkupMarkType, + MarkupNode, + MarkupNodeType, + markupToJSON, + nodeDoc, + nodeParagraph, + nodeText +} from '@hcengineering/text-core' +import { Editor, getSchema } from '@tiptap/core' +import { ServerKit } from '../../kits/server-kit' +import { getMarkup, htmlToJSON, htmlToMarkup, jsonToHTML, jsonToPmNode, jsonToText, pmNodeToJSON } from '../utils' + +// mock tiptap functions +jest.mock('@tiptap/html', () => ({ + generateHTML: jest.fn(() => '

hello

'), + generateJSON: jest.fn(() => ({ + type: 'doc', + content: [{ type: 'paragraph', content: [{ type: 'text', text: 'hello' }] }] + })) +})) + +const extensions = [ServerKit] + +describe('EmptyMarkup', () => { + it('is empty markup', async () => { + const editor = new Editor({ extensions }) + expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy() + }) +}) + +describe('getMarkup', () => { + it('with empty content', async () => { + const editor = new Editor({ extensions }) + expect(getMarkup(editor)).toEqual('{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null}}]}') + }) + it('with some content', async () => { + const editor = new Editor({ extensions, content: '

hello

' }) + expect(getMarkup(editor)).toEqual( + '{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null},"content":[{"type":"text","text":"hello"}]}]}' + ) + }) + it('with empty paragraphs as content', async () => { + const editor = new Editor({ extensions, content: '

' }) + expect(getMarkup(editor)).toEqual( + '{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null}},{"type":"paragraph","attrs":{"textAlign":null}}]}' + ) + }) +}) + +describe('isEmptyMarkup', () => { + it('returns true for undefined content', async () => { + expect(isEmptyMarkup(undefined)).toBeTruthy() + expect(isEmptyMarkup('')).toBeTruthy() + }) + it('returns true for empty content', async () => { + const editor = new Editor({ extensions }) + expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy() + }) + it('returns true for empty paragraphs content', async () => { + const editor = new Editor({ extensions, content: '

' }) + expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy() + }) + it('returns true for empty paragraphs content with spaces', async () => { + const editor = new Editor({ extensions, content: '

' }) + expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy() + }) + it('returns false for not empty content', async () => { + const editor = new Editor({ extensions, content: '

hello

' }) + expect(isEmptyMarkup(getMarkup(editor))).toBeFalsy() + }) + it('returns true for various empty content', async () => { + expect(isEmptyMarkup(jsonToMarkup({ type: MarkupNodeType.doc }))).toBeTruthy() + expect(isEmptyMarkup(jsonToMarkup({ type: MarkupNodeType.doc, content: [] }))).toBeTruthy() + expect( + isEmptyMarkup(jsonToMarkup({ type: MarkupNodeType.doc, content: [{ type: MarkupNodeType.paragraph }] })) + ).toBeTruthy() + expect( + isEmptyMarkup( + jsonToMarkup({ type: MarkupNodeType.doc, content: [{ type: MarkupNodeType.paragraph, content: [] }] }) + ) + ).toBeTruthy() + }) +}) + +describe('areEqualMarkups', () => { + it('returns true for the same content', async () => { + const markup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}' + expect(areEqualMarkups(markup, markup)).toBeTruthy() + }) + it('returns true for empty content', async () => { + expect( + areEqualMarkups('{"type":"doc","content":[]}', '{"type":"doc","content":[{"type":"paragraph"}]}') + ).toBeTruthy() + expect( + areEqualMarkups( + '{"type":"doc","content":[{"type":"paragraph"}]}', + '{"type":"doc","content":[{"type":"paragraph","content":[]}]}' + ) + ).toBeTruthy() + }) + it('returns true for same content but empty marks and attrs', async () => { + expect( + areEqualMarkups( + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}', + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello","content":[],"marks":[],"attrs": {"color": null}}]}]}' + ) + ).toBeTruthy() + }) + it('returns false for same content but trailing hard breaks', async () => { + expect( + areEqualMarkups( + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello","marks":[]}]}]}', + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"},{"type":"hardBreak"}]}]}' + ) + ).toBeFalsy() + expect( + areEqualMarkups( + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}', + '{"type":"doc","content":[{"type":"hardBreak"},{"type":"paragraph","content":[{"type":"text","text":"hello"}]},{"type":"hardBreak"}]}' + ) + ).toBeFalsy() + }) + it('returns false for different content', async () => { + expect( + areEqualMarkups( + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}', + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"world"}]}]}' + ) + ).toBeFalsy() + }) + it('returns false for different marks', async () => { + expect( + areEqualMarkups( + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello","marks":[{"type":"bold"}]}]}]}', + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello","marks":[{"type":"italic"}]}]}]}' + ) + ).toBeFalsy() + }) +}) + +describe('isEmptyNode', () => { + it('returns true for empty doc node', () => { + const node: MarkupNode = { + type: MarkupNodeType.doc, + content: [] + } + expect(isEmptyNode(node)).toBeTruthy() + }) + + it('returns true for empty paragraph node', () => { + const node: MarkupNode = { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [] + } + ] + } + expect(isEmptyNode(node)).toBeTruthy() + }) + + it('returns true for empty text node', () => { + const node: MarkupNode = { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: '' + } + ] + } + ] + } + expect(isEmptyNode(node)).toBeTruthy() + }) + + it('returns false for non-empty text node', () => { + const node: MarkupNode = { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'Hello, world!' + } + ] + } + expect(isEmptyNode(node)).toBeFalsy() + }) + + it('returns false for non-empty text node', () => { + const node: MarkupNode = { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.horizontal_rule + } + ] + } + expect(isEmptyNode(node)).toBeFalsy() + }) + + it('returns false for non-empty node', () => { + const node: MarkupNode = { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'Hello, world!' + } + ] + } + expect(isEmptyNode(node)).toBeFalsy() + }) +}) + +describe('markupToJSON', () => { + it('with empty content', async () => { + expect(markupToJSON('')).toEqual({ type: 'doc', content: [{ type: 'paragraph', content: [] }] }) + }) + it('with some content', async () => { + const markup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}' + expect(markupToJSON(markup)).toEqual({ + type: 'doc', + content: [{ type: 'paragraph', content: [{ type: 'text', text: 'hello' }] }] + }) + }) +}) + +describe('jsonToMarkup', () => { + it('with some content', async () => { + const json: MarkupNode = { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'hello' + } + ] + } + ] + } + expect(jsonToMarkup(json)).toEqual( + '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}' + ) + }) +}) + +describe('pmNodeToJSON', () => { + it('converts ProseMirrorNode to Markup', () => { + const schema = getSchema(extensions) + const node = schema.node('paragraph', {}, [schema.text('Hello, world!')]) + + const json: MarkupNode = { + type: MarkupNodeType.paragraph, + attrs: { textAlign: null as any }, + content: [nodeText('Hello, world!')] + } + expect(pmNodeToJSON(node)).toEqual(json) + }) +}) + +describe('jsonToPmNode', () => { + it('converts json to ProseMirrorNode', () => { + const markup = '{"type":"paragraph","content":[{"type":"text","text":"Hello, world!"}]}' + const node = jsonToPmNode(markupToJSON(markup)) + + expect(node.type.name).toEqual('paragraph') + expect(node.content.childCount).toEqual(1) + expect(node.content.child(0).type.name).toEqual('text') + expect(node.content.child(0).text).toEqual('Hello, world!') + }) +}) + +describe('htmlToMarkup', () => { + it('converts HTML to Markup', () => { + const html = '

hello

' + const expectedMarkup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}' + expect(htmlToMarkup(html)).toEqual(expectedMarkup) + }) +}) + +describe('htmlToJSON', () => { + it('converts HTML to JSON', () => { + const html = '

hello

' + const json = nodeDoc(nodeParagraph(nodeText('hello'))) + expect(htmlToJSON(html)).toEqual(json) + }) +}) + +describe('jsonToHTML', () => { + it('converts JSON to HTML', () => { + const json = nodeDoc(nodeParagraph(nodeText('hello'))) + const html = '

hello

' + expect(jsonToHTML(json)).toEqual(html) + }) +}) + +describe('jsonToText', () => { + it('returns text for text node', () => { + const node: MarkupNode = { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'Hello, world!' + } + ] + } + expect(jsonToText(node)).toEqual('Hello, world!') + }) + it('returns concatenated text for block node with multiple children', () => { + const node: MarkupNode = { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'Hello ' + }, + { + type: MarkupNodeType.text, + text: 'world!' + } + ] + } + expect(jsonToText(node)).toEqual('Hello world!') + }) + it('returns text for node with link', () => { + const node: MarkupNode = { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'Hello! Check out ' + }, + { + type: MarkupNodeType.text, + text: 'this page', + marks: [ + { + type: MarkupMarkType.link, + attrs: { + href: 'http://example.com/' + } + } + ] + }, + { + type: MarkupNodeType.text, + text: '!' + } + ] + } + expect(jsonToText(node)).toEqual('Hello! Check out this page!') + }) + it('returns empty string for block node with no children', () => { + const node: MarkupNode = { + type: MarkupNodeType.paragraph, + content: [] + } + expect(jsonToText(node)).toEqual('') + }) + it('returns error for text node with no text', () => { + const node: MarkupNode = { + type: MarkupNodeType.text, + text: '' + } + expect(() => jsonToText(node)).toThrow('Empty text nodes are not allowed') + }) + it('returns error for block node with empty children', () => { + const node: MarkupNode = { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: '' + }, + { + type: MarkupNodeType.text, + text: '' + } + ] + } + expect(() => jsonToText(node)).toThrow('Empty text nodes are not allowed') + }) +}) diff --git a/foundations/core/packages/text/src/markup/utils.ts b/foundations/core/packages/text/src/markup/utils.ts new file mode 100644 index 0000000000..0746171454 --- /dev/null +++ b/foundations/core/packages/text/src/markup/utils.ts @@ -0,0 +1,80 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Markup } from '@hcengineering/core' +import { Editor, Extensions, getSchema } from '@tiptap/core' +import { generateHTML, generateJSON } from '@tiptap/html' +import { Node as ProseMirrorNode, Schema } from '@tiptap/pm/model' + +import { MarkupNode, jsonToMarkup } from '@hcengineering/text-core' +import { defaultExtensions } from '../extensions' + +/** @public */ +const defaultSchema = getSchema(defaultExtensions) + +/** @public */ +export function getMarkup (editor?: Editor): Markup { + return jsonToMarkup(editor?.getJSON() as MarkupNode) +} + +// Markup + +/** @public */ +export function jsonToPmNode (json: MarkupNode, schema?: Schema, extensions?: Extensions): ProseMirrorNode { + schema ??= extensions == null ? defaultSchema : getSchema(extensions ?? defaultExtensions) + return ProseMirrorNode.fromJSON(schema, json) +} + +/** @public */ +export function pmNodeToJSON (node: ProseMirrorNode): MarkupNode { + return node.toJSON() +} + +/** @public */ +export function jsonToText (node: MarkupNode, schema?: Schema, extensions?: Extensions): string { + const pmNode = jsonToPmNode(node, schema, extensions) + return pmNode.textBetween(0, pmNode.content.size, '\n', '') +} + +// export function markupToText (markup: Markup, schema?: Schema, extensions?: Extensions): string { +// const pmNode = markupToPmNode(markup, schema, extensions) +// return pmNode.textBetween(0, pmNode.content.size, '\n', '') +// } + +// HTML + +/** @public */ +export function htmlToMarkup (html: string, extensions?: Extensions): Markup { + const json = htmlToJSON(html, extensions) + return jsonToMarkup(json) +} + +// /** @public */ +// export function markupToHTML (markup: Markup, extensions?: Extensions): string { +// const json = markupToJSON(markup) +// return jsonToHTML(json, extensions) +// } + +/** @public */ +export function htmlToJSON (html: string, extensions?: Extensions): MarkupNode { + extensions = extensions ?? defaultExtensions + return generateJSON(html, extensions, { preserveWhitespace: 'full' }) as MarkupNode +} + +/** @public */ +export function jsonToHTML (json: MarkupNode, extensions?: Extensions): string { + extensions = extensions ?? defaultExtensions + return generateHTML(json, extensions) +} diff --git a/foundations/core/packages/text/src/nodes/codeblock.ts b/foundations/core/packages/text/src/nodes/codeblock.ts new file mode 100644 index 0000000000..29e686ee29 --- /dev/null +++ b/foundations/core/packages/text/src/nodes/codeblock.ts @@ -0,0 +1,88 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { textblockTypeInputRule } from '@tiptap/core' +import CodeBlock, { CodeBlockOptions } from '@tiptap/extension-code-block' + +export const codeBlockOptions: CodeBlockOptions = { + defaultLanguage: 'plaintext', + languageClassPrefix: 'language-', + exitOnArrowDown: true, + exitOnTripleEnter: true, + HTMLAttributes: { + class: 'proseCodeBlock' + } +} + +/** + * Matches a code block with backticks. + */ +export const backtickInputRegex = /^```$/ + +/** + * Matches a code block with tildes. + */ +export const tildeInputRegex = /^~~~$/ + +export const CodeBlockExtension = CodeBlock.extend({ + marks: 'inline-comment', + + addAttributes () { + return { + language: { + default: null, + parseHTML: (element) => { + const { languageClassPrefix } = this.options + let fchild = element.firstElementChild + if (fchild == null) { + for (const c of element.childNodes) { + if (c.nodeType === 1) { + // According to https://developer.mozilla.org/en-US/docs/Web/API/Node + fchild = c as Element + } + } + } + const classNames = [...Array.from(fchild?.classList ?? [])] + if (classNames.length === 0 && fchild?.className !== undefined) { + classNames.push(fchild?.className) + } + const languages = classNames + .filter((className) => className.startsWith(languageClassPrefix)) + .map((className) => className.replace(languageClassPrefix, '')) + const language = languages[0] + + if (language == null) { + return null + } + + return language + }, + rendered: false + } + } + }, + addInputRules () { + return [ + textblockTypeInputRule({ + find: backtickInputRegex, + type: this.type + }), + textblockTypeInputRule({ + find: tildeInputRegex, + type: this.type + }) + ] + } +}) diff --git a/foundations/core/packages/text/src/nodes/comment.ts b/foundations/core/packages/text/src/nodes/comment.ts new file mode 100644 index 0000000000..9bec970fa6 --- /dev/null +++ b/foundations/core/packages/text/src/nodes/comment.ts @@ -0,0 +1,39 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Node } from '@tiptap/core' + +/** + * @public + */ +export const CommentNode = Node.create({ + name: 'comment', + group: 'inline', + inline: true, + content: 'text*', + marks: '_', + + parseHTML () { + return [ + { + tag: 'comment' + } + ] + }, + + renderText () { + return '' + } +}) diff --git a/foundations/core/packages/text/src/nodes/embed.ts b/foundations/core/packages/text/src/nodes/embed.ts new file mode 100644 index 0000000000..401c1e02ca --- /dev/null +++ b/foundations/core/packages/text/src/nodes/embed.ts @@ -0,0 +1,50 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { mergeAttributes, Node } from '@tiptap/core' + +export const EmbedNode = Node.create({ + name: 'embed', + + addOptions () { + return {} + }, + + inline: false, + group: 'block', + atom: false, + draggable: false, + + addAttributes () { + return { + src: { + default: null + } + } + }, + + parseHTML () { + return [ + { + priority: 60, + tag: `figure[data-type="${this.name}"] iframe[src]` + } + ] + }, + + renderHTML ({ HTMLAttributes }) { + return ['figure', { 'data-type': this.name }, ['iframe', mergeAttributes(HTMLAttributes)]] + } +}) diff --git a/foundations/core/packages/text/src/nodes/emoji.ts b/foundations/core/packages/text/src/nodes/emoji.ts new file mode 100644 index 0000000000..96fb909ae7 --- /dev/null +++ b/foundations/core/packages/text/src/nodes/emoji.ts @@ -0,0 +1,143 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Node, mergeAttributes } from '@tiptap/core' +import type { Blob, Ref } from '@hcengineering/core' + +declare module '@tiptap/core' { + interface Commands { + emoji: { + insertEmoji: (emoji: string, kind: 'unicode' | 'image', image?: Ref) => ReturnType + } + } +} + +export interface EmojiNodeOptions { + getBlobRef: (fileId: Ref, filename?: string, size?: number) => Promise<{ src: string, srcset: string }> +} + +export const EmojiNode = Node.create({ + name: 'emoji', + group: 'inline', + inline: true, + atom: true, + selectable: false, + + addAttributes () { + return { + emoji: { + default: '' + }, + kind: { + default: 'unicode' + }, + image: { + default: null + } + } + }, + + addCommands () { + return { + insertEmoji: + (emoji: string, kind: 'unicode' | 'image', image?: Ref) => + ({ commands }) => { + if (kind === 'image') emoji = `:${emoji}:` + return commands.insertContent({ + type: this.name, + attrs: { emoji, kind, image } + }) + } + } + }, + + parseHTML () { + return [ + { + tag: `span[data-type="${this.name}"]` + } + ] + }, + + addNodeView () { + return ({ node, HTMLAttributes }) => { + const container = document.createElement('span') + const containerAttributes = mergeAttributes( + { + 'data-type': this.name, + class: 'emoji' + }, + HTMLAttributes + ) + + for (const [k, v] of Object.entries(containerAttributes)) { + if (v !== null) { + container.setAttribute(k, v) + } + } + + if (node.attrs.kind === 'image') { + const imgElement = document.createElement('img') + imgElement.alt = node.attrs.emoji + imgElement.setAttribute('data-type', this.name) + void this.options.getBlobRef(node.attrs.image).then((val) => { + imgElement.src = val.src + imgElement.srcset = val.srcset + }) + container.append(imgElement) + } else { + container.append(node.attrs.emoji) + } + + return { + dom: container + } + } + }, + + renderHTML ({ node, HTMLAttributes }) { + if (node.attrs.kind === 'image') { + return [ + 'span', + mergeAttributes( + { + 'data-type': this.name, + class: 'emoji' + }, + HTMLAttributes + ), + [ + 'img', + mergeAttributes({ + 'data-type': this.name, + src: node.attrs.image, + alt: node.attrs.emoji + }) + ] + ] + } + return [ + 'span', + mergeAttributes( + { + 'data-type': this.name, + class: 'emoji' + }, + HTMLAttributes + ), + node.attrs.emoji + ] + } +}) diff --git a/foundations/core/packages/text/src/nodes/file.ts b/foundations/core/packages/text/src/nodes/file.ts new file mode 100644 index 0000000000..8f88dd3305 --- /dev/null +++ b/foundations/core/packages/text/src/nodes/file.ts @@ -0,0 +1,102 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { Node } from '@tiptap/core' + +/** + * @public + */ +export interface FileOptions { + inline: boolean + HTMLAttributes: Record +} + +/** + * @public + */ +export const FileNode = Node.create({ + name: 'file', + + addOptions () { + return { + inline: true, + HTMLAttributes: {} + } + }, + + inline () { + return this.options.inline + }, + + group () { + return this.options.inline ? 'inline' : 'block' + }, + + draggable: true, + + selectable: true, + + addAttributes () { + return { + 'file-id': { + default: null + }, + 'data-file-name': { + default: null + }, + 'data-file-size': { + default: null + }, + 'data-file-type': { + default: null + }, + 'data-file-href': { + default: null + } + } + }, + + parseHTML () { + return [ + { + tag: `div[data-type="${this.name}"]` + } + ] + }, + + renderHTML ({ node, HTMLAttributes }) { + const nodeAttributes = { + 'data-type': this.name + } + + const fileName = HTMLAttributes['data-file-name'] + const size = HTMLAttributes['data-file-size'] + const fileType = HTMLAttributes['data-file-type'] + const href = HTMLAttributes['data-file-href'] + const linkAttributes = { + class: 'file-name', + href, + type: fileType, + download: fileName, + target: '_blank' + } + + return [ + 'div', + nodeAttributes, + ['div', {}, ['a', linkAttributes, `${fileName} (${fileType})`]], + ['div', {}, `${size}`] + ] + } +}) diff --git a/foundations/core/packages/text/src/nodes/image.ts b/foundations/core/packages/text/src/nodes/image.ts new file mode 100644 index 0000000000..ae29f14042 --- /dev/null +++ b/foundations/core/packages/text/src/nodes/image.ts @@ -0,0 +1,147 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import type { Blob, Ref } from '@hcengineering/core' +import { Node, mergeAttributes } from '@tiptap/core' +import { getDataAttribute } from './utils' + +/** + * @public + */ +export type ImageAlignment = 'center' | 'left' | 'right' + +export interface ImageAlignmentOptions { + align?: ImageAlignment +} + +export interface ImageSizeOptions { + height?: number | string + width?: number | string +} + +declare module '@tiptap/core' { + export interface Commands { + image: { + /** + * Add an image + */ + setImage: (options: { src: string, alt?: string, title?: string }) => ReturnType + /** + * Set image alignment + */ + setImageAlignment: (options: ImageAlignmentOptions) => ReturnType + /** + * Set image size + */ + setImageSize: (options: ImageSizeOptions) => ReturnType + } + } +} + +/** + * @public + */ +export interface ImageOptions { + inline: boolean + HTMLAttributes: Record + + loadingImgSrc?: string + getBlobRef: (fileId: Ref, filename?: string, size?: number) => Promise<{ src: string, srcset: string }> +} + +/** + * @public + */ +export const ImageNode = Node.create({ + name: 'image', + + addOptions () { + return { + inline: true, + HTMLAttributes: {}, + getBlobRef: async () => ({ src: '', srcset: '' }) + } + }, + + inline () { + return this.options.inline + }, + + group () { + return this.options.inline ? 'inline' : 'block' + }, + + draggable: true, + + selectable: true, + + addAttributes () { + return { + 'file-id': { + default: null + }, + width: { + default: null + }, + height: { + default: null + }, + src: { + default: null + }, + alt: { + default: null + }, + title: { + default: null + }, + align: getDataAttribute('align'), + 'data-file-type': { + default: null + } + } + }, + + parseHTML () { + return [ + { + tag: `img[data-type="${this.name}"]` + }, + { + tag: 'img[src]' + } + ] + }, + + renderHTML ({ node, HTMLAttributes }) { + const divAttributes = { + class: 'text-editor-image-container', + 'data-type': this.name, + 'data-align': node.attrs.align + } + const imgAttributes = mergeAttributes( + { + 'data-type': this.name + }, + this.options.HTMLAttributes, + HTMLAttributes + ) + const fileId = imgAttributes['file-id'] + if (fileId != null) { + imgAttributes.src = `platform://platform/files/workspace/?file=${fileId}` + } + + return ['div', divAttributes, ['img', imgAttributes]] + } +}) diff --git a/foundations/core/packages/text/src/nodes/index.ts b/foundations/core/packages/text/src/nodes/index.ts new file mode 100644 index 0000000000..11bd4eb863 --- /dev/null +++ b/foundations/core/packages/text/src/nodes/index.ts @@ -0,0 +1,25 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './image' +export * from './reference' +export * from './emoji' +export * from './todo' +export * from './file' +export * from './codeblock' +export * from './comment' +export * from './markdown' +export * from './embed' +export { getDataAttribute } from './utils' diff --git a/foundations/core/packages/text/src/nodes/markdown.ts b/foundations/core/packages/text/src/nodes/markdown.ts new file mode 100644 index 0000000000..dd3e2f2194 --- /dev/null +++ b/foundations/core/packages/text/src/nodes/markdown.ts @@ -0,0 +1,42 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { mergeAttributes, Node } from '@tiptap/core' + +export const MarkdownNode = Node.create({ + name: 'markdown', + group: 'block', + content: 'text*', + marks: '', + code: true, + defining: true, + + parseHTML () { + return [ + { + tag: 'pre[data-type="markdown"]', + preserveWhitespace: 'full' + } + ] + }, + + renderHTML ({ node, HTMLAttributes }) { + return [ + 'pre', + mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), + ['code', {}, 0] + ] + } +}) diff --git a/foundations/core/packages/text/src/nodes/mermaid.ts b/foundations/core/packages/text/src/nodes/mermaid.ts new file mode 100644 index 0000000000..4290c60864 --- /dev/null +++ b/foundations/core/packages/text/src/nodes/mermaid.ts @@ -0,0 +1,45 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CodeBlock, { CodeBlockOptions } from '@tiptap/extension-code-block' +import { codeBlockOptions } from './codeblock' + +export const mermaidOptions: CodeBlockOptions = { + ...codeBlockOptions, + defaultLanguage: 'mermaid' +} + +export const MermaidExtension = CodeBlock.extend({ + name: 'mermaid', + group: 'block', + marks: 'inline-comment', + + parseHTML () { + return [ + { + tag: 'div.mermaid-diagram', + preserveWhitespace: 'full' + } + ] + }, + + addAttributes () { + return { + language: { + default: 'mermaid' + } + } + } +}) diff --git a/foundations/core/packages/text/src/nodes/reference.ts b/foundations/core/packages/text/src/nodes/reference.ts new file mode 100644 index 0000000000..613a51eca1 --- /dev/null +++ b/foundations/core/packages/text/src/nodes/reference.ts @@ -0,0 +1,104 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Node, mergeAttributes } from '@tiptap/core' +import { getDataAttribute } from './utils' +import { Class, Doc, Ref } from '@hcengineering/core' +import { Attrs } from '@tiptap/pm/model' + +export interface ReferenceNodeProps { + id: Ref + objectclass: Ref> + label: string +} + +export interface ReferenceOptions { + suggestion: { char?: string } + HTMLAttributes: Record +} + +/** + * @public + */ +export const ReferenceNode = Node.create({ + name: 'reference', + group: 'inline', + inline: true, + selectable: true, + + addAttributes () { + return { + id: getDataAttribute('id'), + objectclass: getDataAttribute('objectclass'), + label: getDataAttribute('label') + } + }, + + addOptions () { + return { + suggestion: { char: '@' }, + HTMLAttributes: {} + } + }, + + parseHTML () { + return [ + { + priority: 60, + tag: 'span[data-type="reference"]', + getAttrs + }, + { + priority: 60, + tag: 'a[data-type="reference"]', + getAttrs + } + ] + }, + + renderHTML ({ node, HTMLAttributes }) { + return [ + 'span', + mergeAttributes( + { + 'data-type': this.name, + 'data-id': node.attrs.id, + 'data-objectclass': node.attrs.objectclass, + 'data-label': node.attrs.label, + class: 'antiMention' + }, + this.options.HTMLAttributes, + HTMLAttributes + ), + `${this.options.suggestion.char}${node.attrs.label ?? node.attrs.id}` + ] + } +}) + +function getAttrs (el: HTMLSpanElement): Attrs | false { + const id = el.dataset.id?.trim() + const label = el.dataset.label?.trim() + const objectclass = el.dataset.objectclass?.trim() + + if (id == null || label == null || objectclass == null) { + return false + } + + return { + id, + label, + objectclass + } +} diff --git a/foundations/core/packages/text/src/nodes/todo.ts b/foundations/core/packages/text/src/nodes/todo.ts new file mode 100644 index 0000000000..f7d28ba147 --- /dev/null +++ b/foundations/core/packages/text/src/nodes/todo.ts @@ -0,0 +1,36 @@ +import { TaskItem } from '@tiptap/extension-task-item' +import { TaskList } from '@tiptap/extension-task-list' + +import { getDataAttribute } from './utils' + +export const TodoItemNode = TaskItem.extend({ + name: 'todoItem', + group: 'listItems', + + addOptions () { + return { + nested: true, + HTMLAttributes: {}, + taskListTypeName: 'todoList' + } + }, + + addAttributes () { + return { + ...this.parent?.(), + todoid: getDataAttribute('todoid', { default: null, keepOnSplit: false }), + userid: getDataAttribute('userid', { default: null, keepOnSplit: false }) + } + } +}) + +export const TodoListNode = TaskList.extend({ + name: 'todoList', + + addOptions () { + return { + itemTypeName: 'todoItem', + HTMLAttributes: {} + } + } +}) diff --git a/foundations/core/packages/text/src/nodes/utils.ts b/foundations/core/packages/text/src/nodes/utils.ts new file mode 100644 index 0000000000..0e051f1520 --- /dev/null +++ b/foundations/core/packages/text/src/nodes/utils.ts @@ -0,0 +1,41 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Attribute } from '@tiptap/core' + +/** + * @public + */ +export function getDataAttribute ( + name: string, + options?: Partial> +): Partial { + const dataName = `data-${name}` + + return { + default: null, + parseHTML: (element) => element.getAttribute(dataName), + renderHTML: (attributes) => { + if (attributes[name] == null) { + return null + } + + return { + [dataName]: attributes[name] + } + }, + ...(options ?? {}) + } +} diff --git a/foundations/core/packages/text/src/tiptapExtensions.ts b/foundations/core/packages/text/src/tiptapExtensions.ts new file mode 100644 index 0000000000..ff889ad401 --- /dev/null +++ b/foundations/core/packages/text/src/tiptapExtensions.ts @@ -0,0 +1,43 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export { TextStyle, type TextStyleOptions } from '@tiptap/extension-text-style' +export { Blockquote, type BlockquoteOptions } from '@tiptap/extension-blockquote' +export { Bold, type BoldOptions } from '@tiptap/extension-bold' +export { Document } from '@tiptap/extension-document' +export { Dropcursor, type DropcursorOptions } from '@tiptap/extension-dropcursor' +export { Gapcursor } from '@tiptap/extension-gapcursor' +export { HardBreak, type HardBreakOptions } from '@tiptap/extension-hard-break' +export { Heading, type HeadingOptions } from '@tiptap/extension-heading' +export { History, type HistoryOptions } from '@tiptap/extension-history' +export { HorizontalRule, type HorizontalRuleOptions } from '@tiptap/extension-horizontal-rule' +export { Italic, type ItalicOptions } from '@tiptap/extension-italic' +export { Paragraph, type ParagraphOptions } from '@tiptap/extension-paragraph' +export { Strike, type StrikeOptions } from '@tiptap/extension-strike' +export { Text } from '@tiptap/extension-text' +export { Link, type LinkOptions } from '@tiptap/extension-link' +export { Typography, type TypographyOptions } from '@tiptap/extension-typography' +export { Underline, type UnderlineOptions } from '@tiptap/extension-underline' +export { BulletList, type BulletListOptions } from '@tiptap/extension-bullet-list' +export { ListItem, type ListItemOptions } from '@tiptap/extension-list-item' +export { OrderedList, type OrderedListOptions } from '@tiptap/extension-ordered-list' +export { TextAlign, type TextAlignOptions } from '@tiptap/extension-text-align' +export { TaskList, type TaskListOptions } from '@tiptap/extension-task-list' + +export { Table, type TableOptions } from '@tiptap/extension-table' +export { TableCell, type TableCellOptions } from '@tiptap/extension-table-cell' +export { TableHeader, type TableHeaderOptions } from '@tiptap/extension-table-header' +export { TableRow, type TableRowOptions } from '@tiptap/extension-table-row' +export { TaskItem, type TaskItemOptions } from '@tiptap/extension-task-item' diff --git a/foundations/core/packages/text/tsconfig.json b/foundations/core/packages/text/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/text/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/packages/token/.eslintrc.js b/foundations/core/packages/token/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/foundations/core/packages/token/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/foundations/core/packages/token/.npmignore b/foundations/core/packages/token/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/foundations/core/packages/token/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/foundations/core/packages/token/CHANGELOG.json b/foundations/core/packages/token/CHANGELOG.json new file mode 100644 index 0000000000..a4d312f15d --- /dev/null +++ b/foundations/core/packages/token/CHANGELOG.json @@ -0,0 +1,69 @@ +{ + "name": "@hcengineering/server-token", + "entries": [ + { + "version": "0.7.17", + "tag": "@hcengineering/server-token_v0.7.17", + "date": "Mon, 27 Oct 2025 13:27:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" + } + ] + } + }, + { + "version": "0.7.5", + "tag": "@hcengineering/server-token_v0.7.5", + "date": "Tue, 14 Oct 2025 04:58:17 GMT", + "comments": { + "patch": [ + { + "comment": "update deps" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" + } + ] + } + }, + { + "version": "0.7.4", + "tag": "@hcengineering/server-token_v0.7.4", + "date": "Sat, 11 Oct 2025 18:20:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update to latest platform rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" + }, + { + "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" + } + ] + } + }, + { + "version": "0.7.3", + "tag": "@hcengineering/server-token_v0.7.3", + "date": "Wed, 08 Oct 2025 03:40:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" + } + ] + } + } + ] +} diff --git a/foundations/core/packages/token/CHANGELOG.md b/foundations/core/packages/token/CHANGELOG.md new file mode 100644 index 0000000000..39e2bc934e --- /dev/null +++ b/foundations/core/packages/token/CHANGELOG.md @@ -0,0 +1,28 @@ +# Change Log - @hcengineering/server-token + +This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. + +## 0.7.17 +Mon, 27 Oct 2025 13:27:12 GMT + +_Version update only_ + +## 0.7.5 +Tue, 14 Oct 2025 04:58:17 GMT + +### Patches + +- update deps + +## 0.7.4 +Sat, 11 Oct 2025 18:20:33 GMT + +### Patches + +- Update to latest platform rig + +## 0.7.3 +Wed, 08 Oct 2025 03:40:53 GMT + +_Initial release_ + diff --git a/foundations/core/packages/token/config/rig.json b/foundations/core/packages/token/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/foundations/core/packages/token/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/foundations/core/packages/token/jest.config.js b/foundations/core/packages/token/jest.config.js new file mode 100644 index 0000000000..069ea8aa0d --- /dev/null +++ b/foundations/core/packages/token/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html', 'lcov'] +} diff --git a/foundations/core/packages/token/package.json b/foundations/core/packages/token/package.json new file mode 100644 index 0000000000..f5683085a0 --- /dev/null +++ b/foundations/core/packages/token/package.json @@ -0,0 +1,61 @@ +{ + "name": "@hcengineering/server-token", + "version": "0.7.17", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__test__/**", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent --coverage", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --coverage", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@types/node": "^22.18.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/uuid": "^8.3.1", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.22", + "@hcengineering/platform": "workspace:^0.7.18", + "jwt-simple": "^0.5.6", + "uuid": "^8.3.2" + }, + "repository": "https://github.com/hcengineering/huly.core", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/foundations/core/packages/token/src/__tests__/token.test.ts b/foundations/core/packages/token/src/__tests__/token.test.ts new file mode 100644 index 0000000000..fc54e62991 --- /dev/null +++ b/foundations/core/packages/token/src/__tests__/token.test.ts @@ -0,0 +1,116 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { setMetadata } from '@hcengineering/platform' +import type { PersonUuid, WorkspaceUuid } from '@hcengineering/core' +import { decodeToken, generateToken } from '../token' +import plugin from '../plugin' + +export function decodeTokenPayload (token: string): any { + try { + return JSON.parse(atob(token.split('.')[1])) + } catch (err: any) { + console.error(err) + return {} + } +} + +describe('generateToken', () => { + beforeEach(() => { + setMetadata(plugin.metadata.Secret, undefined) + setMetadata(plugin.metadata.Service, undefined) + }) + + it('throws TokenError for invalid account uuid', () => { + expect(() => { + generateToken('invalid-uuid' as PersonUuid, '' as WorkspaceUuid, {}, 'secret') + }).toThrow('Invalid account uuid: "invalid-uuid"') + }) + + it('throws TokenError for invalid workspace uuid', () => { + expect(() => { + generateToken('123e4567-e89b-12d3-a456-426614174000' as PersonUuid, 'invalid-uuid' as WorkspaceUuid, {}, 'secret') + }).toThrow('Invalid workspace uuid: "invalid-uuid"') + }) + + it('generates token without extra and workspace', () => { + const token = generateToken('123e4567-e89b-12d3-a456-426614174000' as PersonUuid, undefined, undefined, 'secret') + const decodedPayload = decodeTokenPayload(token) + expect(decodedPayload).toEqual({ + account: '123e4567-e89b-12d3-a456-426614174000', + workspace: undefined + }) + }) + + it('should generate token with only required fields', () => { + const token = generateToken( + '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, + '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid, + undefined, + 'secret' + ) + const decodedPayload = decodeTokenPayload(token) + expect(decodedPayload).toEqual({ + account: '123e4567-e89b-12d3-a456-426614174000', + workspace: '123e4567-e89b-12d3-a456-426614174001' + }) + }) + + it('should generate token with extra fields', () => { + const extra = { service: 'test' } + const token = generateToken( + '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, + '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid, + extra, + 'secret' + ) + const decodedPayload = decodeTokenPayload(token) + expect(decodedPayload).toEqual({ + extra, + account: '123e4567-e89b-12d3-a456-426614174000', + workspace: '123e4567-e89b-12d3-a456-426614174001' + }) + }) + + it('should generate token with default secret', () => { + const token = generateToken( + '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, + '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid, + undefined, + 'test' + ) + const decodedPayload = decodeTokenPayload(token) + expect(decodedPayload).toEqual({ + account: '123e4567-e89b-12d3-a456-426614174000', + workspace: '123e4567-e89b-12d3-a456-426614174001' + }) + }) + + it('should generate token with default service in extra', () => { + setMetadata(plugin.metadata.Service, 'test') + const token = generateToken( + '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, + '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid, + undefined, + 'secret' + ) + const decodedPayload = decodeToken(token, false, 'test') + expect(decodedPayload).toEqual({ + extra: { service: 'test' }, + account: '123e4567-e89b-12d3-a456-426614174000', + workspace: '123e4567-e89b-12d3-a456-426614174001' + }) + }) +}) diff --git a/foundations/core/packages/token/src/index.ts b/foundations/core/packages/token/src/index.ts new file mode 100644 index 0000000000..dd6f940800 --- /dev/null +++ b/foundations/core/packages/token/src/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export { default } from './plugin' +export * from './token' diff --git a/foundations/core/packages/token/src/plugin.ts b/foundations/core/packages/token/src/plugin.ts new file mode 100644 index 0000000000..67a8f57976 --- /dev/null +++ b/foundations/core/packages/token/src/plugin.ts @@ -0,0 +1,34 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Metadata, Plugin } from '@hcengineering/platform' +import { plugin } from '@hcengineering/platform' + +/** + * @public + */ +export const serverTokenId = 'server-token' as Plugin + +/** + * @public + */ +const serverToken = plugin(serverTokenId, { + metadata: { + Secret: '' as Metadata, + Service: '' as Metadata + } +}) + +export default serverToken diff --git a/foundations/core/packages/token/src/token.ts b/foundations/core/packages/token/src/token.ts new file mode 100644 index 0000000000..7a6ae0eabf --- /dev/null +++ b/foundations/core/packages/token/src/token.ts @@ -0,0 +1,140 @@ +import { AccountRole, AccountUuid, MeasureContext, PersonUuid, WorkspaceUuid } from '@hcengineering/core' +import { getMetadata } from '@hcengineering/platform' +import { decode, encode } from 'jwt-simple' +import { validate } from 'uuid' +import serverPlugin from './plugin' + +/** + * @public + */ +export interface Token { + account: AccountUuid + workspace: WorkspaceUuid + extra?: Record + grant?: PermissionsGrant + + sub?: AccountUuid // Subject + exp?: number // Expiration, seconds since epoch + nbf?: number // Not valid before, seconds since epoch +} + +// Permissions grant provides the token presenter access to a specific workspace +export interface PermissionsGrant { + workspace: WorkspaceUuid + role: AccountRole + + // Ideally we shouldn't need this but for now it's the only way to check + // if some granted permissions are valid - the ones which can only be verified in the workspace + grantedBy?: AccountUuid + + firstName?: string + lastName?: string + + spaces?: string[] + + extra?: Record +} + +/** + * @public + */ +export class TokenError extends Error { + constructor (message: string) { + super(message) + this.name = 'TokenError' + } +} + +const getSecret = (): string => { + return getMetadata(serverPlugin.metadata.Secret) ?? 'secret' +} + +/** + * @public + */ +export function generateToken ( + accountUuid: PersonUuid, + workspaceUuid?: WorkspaceUuid, + extra?: Record, + secret?: string, + options?: { + grant?: PermissionsGrant + nbf?: number + exp?: number + sub?: PersonUuid + } +): string { + if (!validate(accountUuid)) { + throw new TokenError(`Invalid account uuid: "${accountUuid}"`) + } + if (workspaceUuid !== undefined && !validate(workspaceUuid)) { + throw new TokenError(`Invalid workspace uuid: "${workspaceUuid}"`) + } + const { grant, nbf, exp, sub } = options ?? {} + if (grant?.workspace !== undefined && !validate(grant?.workspace)) { + throw new TokenError(`Invalid grant workspace uuid: "${grant?.workspace}"`) + } + + if (grant != null && sub == null && (nbf == null || exp == null)) { + throw new TokenError('nbf and exp are required when sub is not provided') + } + + const service = getMetadata(serverPlugin.metadata.Service) + if (service !== undefined) { + extra = { service, ...extra } + } + + const sanitizedGrant: PermissionsGrant | undefined = + grant !== undefined + ? { + workspace: grant.workspace, + role: grant.role, + grantedBy: grant.grantedBy, + firstName: grant.firstName, + lastName: grant.lastName, + spaces: grant.spaces, + extra: grant.extra + } + : undefined + + return encode( + { + ...(extra !== undefined ? { extra } : {}), + account: accountUuid, + workspace: workspaceUuid, + grant: sanitizedGrant, + sub, + exp, + nbf + }, + secret ?? getSecret() + ) +} + +/** + * @public + */ +export function decodeToken (token: string, verify: boolean = true, secret?: string): Token { + try { + return decode(token, secret ?? getSecret(), !verify) + } catch (err: any) { + throw new TokenError(err.message) + } +} + +/** + * @public + */ +export function decodeTokenVerbose (ctx: MeasureContext, token: string): Token { + try { + return decodeToken(token) + } catch (err: any) { + try { + const decode = decodeToken(token, false) + ctx.warn('Failed to verify token', { ...decode }) + } catch (err2: any) { + // Nothing to do + } + throw new TokenError(err.message) + } +} diff --git a/foundations/core/packages/token/tsconfig.json b/foundations/core/packages/token/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/foundations/core/packages/token/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/foundations/core/rush.json b/foundations/core/rush.json new file mode 100644 index 0000000000..a357322de8 --- /dev/null +++ b/foundations/core/rush.json @@ -0,0 +1,469 @@ +/** + * This is the main configuration file for Rush. + * For full documentation, please see https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", + + /** + * (Required) This specifies the version of the Rush engine to be used in this repo. + * Rush's "version selector" feature ensures that the globally installed tool will + * behave like this release, regardless of which version is installed globally. + * + * The common/scripts/install-run-rush.js automation script also uses this version. + * + * NOTE: If you upgrade to a new major version of Rush, you should replace the "v5" + * path segment in the "$schema" field for all your Rush config files. This will ensure + * correct error-underlining and tab-completion for editors such as VS Code. + */ + "rushVersion": "5.158.1", + + /** + * The next field selects which package manager should be installed and determines its version. + * Rush installs its own local copy of the package manager to ensure that your build process + * is fully isolated from whatever tools are present in the local environment. + * + * Specify one of: "pnpmVersion", "npmVersion", or "yarnVersion". See the Rush documentation + * for details about these alternatives. + */ + "pnpmVersion": "10.15.1", + + // "npmVersion": "6.14.15", + // "yarnVersion": "1.9.4", + + /** + * Older releases of the Node.js engine may be missing features required by your system. + * Other releases may have bugs. In particular, the "latest" version will not be a + * Long Term Support (LTS) version and is likely to have regressions. + * + * Specify a SemVer range to ensure developers use a Node.js version that is appropriate + * for your repo. + * + * LTS schedule: https://nodejs.org/en/about/releases/ + * LTS versions: https://nodejs.org/en/download/releases/ + */ + "nodeSupportedVersionRange": ">=18.20.3 <19.0.0 || >=20.14.0 <25.0.0", + + /** + * If the version check above fails, Rush will display a message showing the current + * node version and the supported version range. You can use this setting to provide + * additional instructions that will display below the warning, if there's a specific + * tool or script you'd like the user to use to get in line with the expected version. + */ + // "nodeSupportedVersionInstructions": "Run 'nvs use' to switch to the expected node version.", + + /** + * Odd-numbered major versions of Node.js are experimental. Even-numbered releases + * spend six months in a stabilization period before the first Long Term Support (LTS) version. + * For example, 8.9.0 was the first LTS version of Node.js 8. Pre-LTS versions are not recommended + * for production usage because they frequently have bugs. They may cause Rush itself + * to malfunction. + * + * Rush normally prints a warning if it detects a pre-LTS Node.js version. If you are testing + * pre-LTS versions in preparation for supporting the first LTS version, you can use this setting + * to disable Rush's warning. + */ + // "suppressNodeLtsWarning": false, + + /** + * Rush normally prints a warning if it detects that the current version is not one published to the + * public npmjs.org registry. If you need to block calls to the npm registry, you can use this setting to disable + * Rush's check. + */ + // "suppressRushIsPublicVersionCheck": false, + + /** + * Large monorepos can become intimidating for newcomers if project folder paths don't follow + * a consistent and recognizable pattern. When the system allows nested folder trees, + * we've found that teams will often use subfolders to create islands that isolate + * their work from others ("shipping the org"). This hinders collaboration and code sharing. + * + * The Rush developers recommend a "category folder" model, where buildable project folders + * must always be exactly two levels below the repo root. The parent folder acts as the category. + * This provides a basic facility for grouping related projects (e.g. "apps", "libraries", + * "tools", "prototypes") while still encouraging teams to organize their projects into + * a unified taxonomy. Limiting to 2 levels seems very restrictive at first, but if you have + * 20 categories and 20 projects in each category, this scheme can easily accommodate hundreds + * of projects. In practice, you will find that the folder hierarchy needs to be rebalanced + * occasionally, but if that's painful, it's a warning sign that your development style may + * discourage refactoring. Reorganizing the categories should be an enlightening discussion + * that brings people together, and maybe also identifies poor coding practices (e.g. file + * references that reach into other project's folders without using Node.js module resolution). + * + * The defaults are projectFolderMinDepth=1 and projectFolderMaxDepth=2. + * + * To remove these restrictions, you could set projectFolderMinDepth=1 + * and set projectFolderMaxDepth to a large number. + */ + // "projectFolderMinDepth": 2, + // "projectFolderMaxDepth": 2, + + /** + * Today the npmjs.com registry enforces fairly strict naming rules for packages, but in the early + * days there was no standard and hardly any enforcement. A few large legacy projects are still using + * nonstandard package names, and private registries sometimes allow it. Set "allowMostlyStandardPackageNames" + * to true to relax Rush's enforcement of package names. This allows upper case letters and in the future may + * relax other rules, however we want to minimize these exceptions. Many popular tools use certain punctuation + * characters as delimiters, based on the assumption that they will never appear in a package name; thus if we relax + * the rules too much it is likely to cause very confusing malfunctions. + * + * The default value is false. + */ + // "allowMostlyStandardPackageNames": true, + + /** + * This feature helps you to review and approve new packages before they are introduced + * to your monorepo. For example, you may be concerned about licensing, code quality, + * performance, or simply accumulating too many libraries with overlapping functionality. + * The approvals are tracked in two config files "browser-approved-packages.json" + * and "nonbrowser-approved-packages.json". See the Rush documentation for details. + */ + // "approvedPackagesPolicy": { + // /** + // * The review categories allow you to say for example "This library is approved for usage + // * in prototypes, but not in production code." + // * + // * Each project can be associated with one review category, by assigning the "reviewCategory" field + // * in the "projects" section of rush.json. The approval is then recorded in the files + // * "common/config/rush/browser-approved-packages.json" and "nonbrowser-approved-packages.json" + // * which are automatically generated during "rush update". + // * + // * Designate categories with whatever granularity is appropriate for your review process, + // * or you could just have a single category called "default". + // */ + // "reviewCategories": [ + // // Some example categories: + // "production", // projects that ship to production + // "tools", // non-shipping projects that are part of the developer toolchain + // "prototypes" // experiments that should mostly be ignored by the review process + // ], + // + // /** + // * A list of NPM package scopes that will be excluded from review. + // * We recommend to exclude TypeScript typings (the "@types" scope), because + // * if the underlying package was already approved, this would imply that the typings + // * are also approved. + // */ + // // "ignoredNpmScopes": ["@types"] + // }, + + /** + * If you use Git as your version control system, this section has some additional + * optional features you can use. + */ + "gitPolicy": { + /** + * Work at a big company? Tired of finding Git commits at work with unprofessional Git + * emails such as "beer-lover@my-college.edu"? Rush can validate people's Git email address + * before they get started. + * + * Define a list of regular expressions describing allowable e-mail patterns for Git commits. + * They are case-insensitive anchored JavaScript RegExps. Example: ".*@example\.com" + * + * IMPORTANT: Because these are regular expressions encoded as JSON string literals, + * RegExp escapes need two backslashes, and ordinary periods should be "\\.". + */ + // "allowedEmailRegExps": [ + // "[^@]+@users\\.noreply\\.github\\.com", + // "rush-bot@example\\.org" + // ], + /** + * When Rush reports that the address is malformed, the notice can include an example + * of a recommended email. Make sure it conforms to one of the allowedEmailRegExps + * expressions. + */ + // "sampleEmail": "example@users.noreply.github.com", + /** + * The commit message to use when committing changes during 'rush publish'. + * + * For example, if you want to prevent these commits from triggering a CI build, + * you might configure your system's trigger to look for a special string such as "[skip-ci]" + * in the commit message, and then customize Rush's message to contain that string. + */ + // "versionBumpCommitMessage": "Bump versions [skip ci]", + /** + * The commit message to use when committing changes during 'rush version'. + * + * For example, if you want to prevent these commits from triggering a CI build, + * you might configure your system's trigger to look for a special string such as "[skip-ci]" + * in the commit message, and then customize Rush's message to contain that string. + */ + // "changeLogUpdateCommitMessage": "Update changelogs [skip ci]", + /** + * The commit message to use when committing changefiles during 'rush change --commit' + * + * If no commit message is set it will default to 'Rush change' + */ + // "changefilesCommitMessage": "Rush change" + }, + + "repository": { + /** + * The URL of this Git repository, used by "rush change" to determine the base branch for your PR. + * + * The "rush change" command needs to determine which files are affected by your PR diff. + * If you merged or cherry-picked commits from the main branch into your PR branch, those commits + * should be excluded from this diff (since they belong to some other PR). In order to do that, + * Rush needs to know where to find the base branch for your PR. This information cannot be + * determined from Git alone, since the "pull request" feature is not a Git concept. Ideally + * Rush would use a vendor-specific protocol to query the information from GitHub, Azure DevOps, etc. + * But to keep things simple, "rush change" simply assumes that your PR is against the "main" branch + * of the Git remote indicated by the repository.url setting in rush.json. If you are working in + * a GitHub "fork" of the real repo, this setting will be different from the repository URL of your + * your PR branch, and in this situation "rush change" will also automatically invoke "git fetch" + * to retrieve the latest activity for the remote main branch. + */ + "url": "https://github.com/hcengineering/huly.core", + /** + * The default branch name. This tells "rush change" which remote branch to compare against. + * The default value is "main" + */ + "defaultBranch": "main", + /** + * The default remote. This tells "rush change" which remote to compare against if the remote URL is + * not set or if a remote matching the provided remote URL is not found. + */ + "defaultRemote": "origin" + }, + + /** + * Event hooks are customized script actions that Rush executes when specific events occur + */ + "eventHooks": { + /** + * A list of shell commands to run before "rush install" or "rush update" starts installation + */ + "preRushInstall": [ + // "common/scripts/pre-rush-install.js" + ], + + /** + * A list of shell commands to run after "rush install" or "rush update" finishes installation + */ + "postRushInstall": [], + + /** + * A list of shell commands to run before "rush build" or "rush rebuild" starts building + */ + "preRushBuild": [], + + /** + * A list of shell commands to run after "rush build" or "rush rebuild" finishes building + */ + "postRushBuild": [], + + /** + * A list of shell commands to run before the "rushx" command starts + */ + "preRushx": [], + + /** + * A list of shell commands to run after the "rushx" command finishes + */ + "postRushx": [] + }, + + /** + * Installation variants allow you to maintain a parallel set of configuration files that can be + * used to build the entire monorepo with an alternate set of dependencies. For example, suppose + * you upgrade all your projects to use a new release of an important framework, but during a transition period + * you intend to maintain compatibility with the old release. In this situation, you probably want your + * CI validation to build the entire repo twice: once with the old release, and once with the new release. + * + * Rush "installation variants" correspond to sets of config files located under this folder: + * + * common/config/rush/variants/ + * + * The variant folder can contain an alternate common-versions.json file. Its "preferredVersions" field can be used + * to select older versions of dependencies (within a loose SemVer range specified in your package.json files). + * To install a variant, run "rush install --variant ". + * + * For more details and instructions, see this article: https://rushjs.io/pages/advanced/installation_variants/ + */ + "variants": [ + // { + // /** + // * The folder name for this variant. + // */ + // "variantName": "old-sdk", + // + // /** + // * An informative description + // */ + // "description": "Build this repo using the previous release of the SDK" + // } + ], + + /** + * Rush can collect anonymous telemetry about everyday developer activity such as + * success/failure of installs, builds, and other operations. You can use this to identify + * problems with your toolchain or Rush itself. THIS TELEMETRY IS NOT SHARED WITH MICROSOFT. + * It is written into JSON files in the common/temp folder. It's up to you to write scripts + * that read these JSON files and do something with them. These scripts are typically registered + * in the "eventHooks" section. + */ + // "telemetryEnabled": false, + + /** + * Allows creation of hotfix changes. This feature is experimental so it is disabled by default. + * If this is set, 'rush change' only allows a 'hotfix' change type to be specified. This change type + * will be used when publishing subsequent changes from the monorepo. + */ + // "hotfixChangeEnabled": false, + + /** + * This is an optional, but recommended, list of allowed tags that can be applied to Rush projects + * using the "tags" setting in this file. This list is useful for preventing mistakes such as misspelling, + * and it also provides a centralized place to document your tags. If "allowedProjectTags" list is + * not specified, then any valid tag is allowed. A tag name must be one or more words + * separated by hyphens or slashes, where a word may contain lowercase ASCII letters, digits, + * ".", and "@" characters. + */ + // "allowedProjectTags": [ "tools", "frontend-team", "1.0.0-release" ], + + /** + * (Required) This is the inventory of projects to be managed by Rush. + * + * Rush does not automatically scan for projects using wildcards, for a few reasons: + * 1. Depth-first scans are expensive, particularly when tools need to repeatedly collect the list. + * 2. On a caching CI machine, scans can accidentally pick up files left behind from a previous build. + * 3. It's useful to have a centralized inventory of all projects and their important metadata. + */ + "projects": [ + { + "packageName": "@hcengineering/scripts", + "projectFolder": "common/scripts", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/account-client", + "projectFolder": "packages/account-client", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/analytics", + "projectFolder": "packages/analytics", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/analytics-service", + "projectFolder": "packages/analytics-service", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/api-client", + "projectFolder": "packages/api-client", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/collaborator-client", + "projectFolder": "packages/collaborator-client", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/core", + "projectFolder": "packages/core", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/model", + "projectFolder": "packages/model", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/platform", + "projectFolder": "packages/platform", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/query", + "projectFolder": "packages/query", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/rank", + "projectFolder": "packages/rank", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/retry", + "projectFolder": "packages/retry", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/storage", + "projectFolder": "packages/storage", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/text", + "projectFolder": "packages/text", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/text-core", + "projectFolder": "packages/text-core", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/text-html", + "projectFolder": "packages/text-html", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/text-markdown", + "projectFolder": "packages/text-markdown", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/text-ydoc", + "projectFolder": "packages/text-ydoc", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/client", + "projectFolder": "packages/client", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/client-resources", + "projectFolder": "packages/client-resources", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/rpc", + "projectFolder": "packages/rpc", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/server-token", + "projectFolder": "packages/token", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/hulylake-client", + "projectFolder": "packages/hulylake-client", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/storage-client", + "projectFolder": "packages/storage-client", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/measurements", + "projectFolder": "packages/measurements", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/measurements-otlp", + "projectFolder": "packages/measurements-otlp", + "shouldPublish": true + }, + { + "packageName": "@hcengineering/postgres-base", + "projectFolder": "packages/postgres-base", + "shouldPublish": true + } + ] +} diff --git a/foundations/hulylake/.dockerignore b/foundations/hulylake/.dockerignore new file mode 100644 index 0000000000..e75469be2c --- /dev/null +++ b/foundations/hulylake/.dockerignore @@ -0,0 +1,4 @@ +target/ +.git/ +.jj/ +.cargo/ \ No newline at end of file diff --git a/foundations/hulylake/.github/workflows/build.yaml b/foundations/hulylake/.github/workflows/build.yaml new file mode 100644 index 0000000000..70ca5b8bd0 --- /dev/null +++ b/foundations/hulylake/.github/workflows/build.yaml @@ -0,0 +1,31 @@ +name: Build and Push + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log to registry + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKER_USER }} + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + + - run: echo VERSION=$(grep '^version =' server/Cargo.toml | cut -d '"' -f 2) >> $GITHUB_ENV + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and Push + uses: docker/build-push-action@v6 + with: + file: Dockerfile + push: true + tags: "${{ vars.DOCKER_USER }}/service_hulylake:${{ env.VERSION }},${{ vars.DOCKER_USER }}/service_hulylake:latest" + platforms: linux/amd64,linux/arm64 diff --git a/foundations/hulylake/.gitignore b/foundations/hulylake/.gitignore new file mode 100644 index 0000000000..9bf4926a2a --- /dev/null +++ b/foundations/hulylake/.gitignore @@ -0,0 +1,3 @@ +/target +.cargo +.vscode \ No newline at end of file diff --git a/foundations/hulylake/Cargo.lock b/foundations/hulylake/Cargo.lock new file mode 100644 index 0000000000..1daeff2723 --- /dev/null +++ b/foundations/hulylake/Cargo.lock @@ -0,0 +1,6649 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.9.3", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-cors" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec", +] + +[[package]] +name = "actix-http" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "base64 0.22.1", + "bitflags 2.9.3", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash", + "futures-core", + "h2 0.3.27", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-tls" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "impl-more", + "pin-project-lite", + "tokio", + "tokio-rustls 0.23.4", + "tokio-util", + "tracing", + "webpki-roots 0.22.6", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie 0.16.2", + "derive_more", + "encoding_rs", + "foldhash", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.5.10", + "time 0.3.42", + "tracing", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi-to-tui" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c" +dependencies = [ + "nom", + "ratatui", + "simdutf8", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +dependencies = [ + "backtrace", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-compression" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977eb15ea9efd848bb8a4a1a2500347ed7f0bf794edf0dc3ddcf439f43d36b23" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "async-tungstenite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f" +dependencies = [ + "atomic-waker", + "futures-core", + "futures-io", + "futures-task", + "futures-util", + "log", + "pin-project-lite", + "tungstenite", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-config" +version = "1.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bc1b40fb26027769f16960d2f4a6bc20c4bb755d403e552c8c1a73af433c246" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.3.1", + "ring 0.17.14", + "time 0.3.42", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d025db5d9f52cbc413b167136afb3d8aeea708c0d8884783cf6253be5e22f6f2" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c034a1bc1d70e16e7f4e4caf7e9f7693e4c9c24cd91cf17c2a0b21abaebc7c8b" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.104.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c488cd6abb0ec9811c401894191932e941c5f91dc226043edacd0afa1634bc" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "lru 0.12.5", + "percent-encoding", + "regex-lite", + "sha2", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.83.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cd43af212d2a1c4dedff6f044d7e1961e5d9e7cfe773d70f31d9842413886" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.84.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ec4a95bd48e0db7a424356a161f8d87bd6a4f0af37204775f0da03d9e39fc3" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.85.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410309ad0df4606bc721aff0d89c3407682845453247213a0ccc5ff8801ee107" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084c34162187d39e3740cb635acd73c4e3a551a36146ad6fe8883c929c9f876c" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.3.1", + "p256", + "percent-encoding", + "ring 0.17.14", + "sha2", + "subtle", + "time 0.3.42", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.63.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d2df0314b8e307995a3b86d44565dfe9de41f876901a7d71886c756a25979f" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 0.2.12", + "http-body 0.4.6", + "md-5", + "pin-project-lite", + "sha1", + "sha2", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "182b03393e8c677347fb5705a04a9392695d47d20ef0a2f8cfe28c8e6b9b9778" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.62.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c4dacf2d38996cf729f55e7a762b30918229917eca115de45dfa8dfb97796c9" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147e8eea63a40315d704b97bf9bc9b8c1402ae94f89d5ad6f7550d963309da1b" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.12", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.7.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.31", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.2", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaa31b350998e703e9826b2104dd6f63be0508666e1aba88137af060e8944047" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3946acbe1ead1301ba6862e712c7903ca9bb230bdf1fbd1b5ac54158ef2ab1f" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07f5e0fc8a6b3f2303f331b94504bbf754d85488f402d6f1dd7a6080f99afe56" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.3.1", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time 0.3.42", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "backon" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bb8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d8b8e1a22743d9241575c6ba822cf9c8fef34771c86ab7e477a4fbfd254e5" +dependencies = [ + "futures-util", + "parking_lot 0.12.4", + "tokio", +] + +[[package]] +name = "bb8-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e570e6557cd0f88d28d32afa76644873271a70dc22656df565b2021c4036aa9c" +dependencies = [ + "bb8", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.9.3", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.106", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +dependencies = [ + "serde", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "bytestring" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +dependencies = [ + "bytes", +] + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.0", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "compression-codecs" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485abf41ac0c8047c07c87c72c8fb3eb5197f6e9d7ded615dfd1a00ae00a0f64" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + +[[package]] +name = "config" +version = "0.15.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0faa974509d38b33ff89282db9c3295707ccf031727c0de9772038ec526852ba" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde-untagged", + "serde_json", + "toml 0.9.5", + "winnow", + "yaml-rust2", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time 0.3.42", + "version_check", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time 0.3.42", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie 0.18.1", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time 0.3.42", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc-fast" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf62af4cc77d8fe1c22dde4e721d87f2f54056139d8c412e1366b740305f56f" +dependencies = [ + "crc", + "digest", + "libc", + "rand 0.9.2", + "regex", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.9.3", + "crossterm_winapi", + "futures-core", + "mio", + "parking_lot 0.12.4", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.11", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.106", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "unicode-xid", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint 0.4.9", + "der", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "font8x8" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.3+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "governor" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" +dependencies = [ + "cfg-if", + "dashmap", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.3", + "hashbrown 0.15.5", + "nonzero_ext", + "parking_lot 0.12.4", + "portable-atomic", + "quanta", + "rand 0.9.2", + "smallvec", + "spinning_top", + "web-time", +] + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.11.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap 2.11.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hulylake" +version = "0.1.18" +dependencies = [ + "actix-cors", + "actix-web", + "anyhow", + "async-stream", + "aws-config", + "aws-sdk-s3", + "bb8", + "bb8-postgres", + "blake3", + "bytes", + "chrono", + "config", + "futures", + "futures-util", + "hulyrs", + "json-patch", + "jsonptr", + "ksuid", + "lockable", + "md5", + "mime", + "opentelemetry", + "opentelemetry-appender-tracing", + "refinery", + "secrecy", + "serde", + "serde_json", + "size", + "strum 0.27.2", + "thiserror 2.0.16", + "tokio", + "tokio-postgres", + "tokio-stream", + "tracing", + "tracing-actix-web", + "tracing-opentelemetry", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "hulyrs" +version = "0.1.0" +source = "git+https://github.com/hcengineering/hulyrs.git#24b211c4d1b013e2ec1e2daae470168a1841dacf" +dependencies = [ + "actix-web", + "bytes", + "chrono", + "config", + "derive_builder", + "futures", + "governor", + "itoa", + "jsonwebtoken", + "num-traits", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry-stdout", + "opentelemetry_sdk", + "rand 0.9.2", + "reqwest", + "reqwest-middleware", + "reqwest-ratelimit", + "reqwest-retry", + "reqwest-websocket", + "ryu", + "secrecy", + "serde", + "serde_json", + "serde_with", + "strum 0.27.2", + "thiserror 2.0.16", + "tokio", + "tokio-stream", + "tokio_with_wasm", + "tracing", + "url", + "uuid", + "wasmtimer", +] + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime", + "serde", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.3.1", + "hyper 1.7.0", + "hyper-util", + "rustls 0.23.31", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.2", + "tower-service", + "webpki-roots 1.0.2", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.7.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.7.0", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.0", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown 0.15.5", + "serde", +] + +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "instability" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "159294d661a039f7644cea7e4d844e6b25aaf71c1ffe9d73a96d768c24b0faf4" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring 0.17.14", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "ksuid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42729258604bc3c02a3502ca8cb070d044fef73d75c657bec0fe5d51bfac8d24" +dependencies = [ + "byteorder", + "rand 0.3.23", + "resize-slice", + "time 0.1.45", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] + +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags 2.9.3", + "libc", + "redox_syscall 0.5.17", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "lockable" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0f35193d57711b7b4730a3b888a5033347fb7be1ee9a64a755fa4ee013ef80" +dependencies = [ + "derive_more", + "futures", + "itertools 0.14.0", + "lru 0.14.0", + "tokio", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memoize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d1d5792299bab3f8b5d88d1b7a7cb50ad7ef039a8c4d45a6b84880a6526276" +dependencies = [ + "lazy_static", + "lru 0.7.8", + "memoize-inner", +] + +[[package]] +name = "memoize-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd8f89255d8ff313afabed9a3c83ef0993cc056679dfd001f5111a026f876f7" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "mutually_exclusive_features" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.9.3", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "opentelemetry" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "opentelemetry-appender-tracing" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68f63eca5fad47e570e00e893094fc17be959c80c79a7d6ec1abdd5ae6ffc16" +dependencies = [ + "opentelemetry", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "opentelemetry-http" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +dependencies = [ + "async-trait", + "bytes", + "http 1.3.1", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +dependencies = [ + "http 1.3.1", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry-stdout" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447191061af41c3943e082ea359ab8b64ff27d6d34d30d327df309ddef1eef6f" +dependencies = [ + "chrono", + "opentelemetry", + "opentelemetry_sdk", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand 0.9.2", + "serde_json", + "thiserror 2.0.16", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "owo-colors" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "papergrid" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b915f831b85d984193fdc3d3611505871dc139b2534530fa01c1a6a6707b6723" +dependencies = [ + "bytecount", + "fnv", + "unicode-width 0.2.0", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.11", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.17", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +dependencies = [ + "memchr", + "thiserror 2.0.16", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pest_meta" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.11.0", + "quick-xml", + "serde", + "time 0.3.42", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "postgres-protocol" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.9.2", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" +dependencies = [ + "bytes", + "chrono", + "fallible-iterator", + "postgres-protocol", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.106", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls 0.23.31", + "socket2 0.6.0", + "thiserror 2.0.16", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring 0.17.14", + "rustc-hash 2.1.1", + "rustls 0.23.31", + "rustls-pki-types", + "slab", + "thiserror 2.0.16", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.0", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.9.3", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools 0.13.0", + "lru 0.12.5", + "paste", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "raw-cpuid" +version = "11.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" +dependencies = [ + "bitflags 2.9.3", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.3", +] + +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "refinery" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba5d693abf62492c37268512ff35b77655d2e957ca53dab85bf993fe9172d15" +dependencies = [ + "refinery-core", + "refinery-macros", +] + +[[package]] +name = "refinery-core" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a83581f18c1a4c3a6ebd7a174bdc665f17f618d79f7edccb6a0ac67e660b319" +dependencies = [ + "async-trait", + "cfg-if", + "log", + "regex", + "serde", + "siphasher", + "thiserror 1.0.69", + "time 0.3.42", + "tokio", + "tokio-postgres", + "toml 0.8.23", + "url", + "walkdir", +] + +[[package]] +name = "refinery-macros" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c225407d8e52ef8cf094393781ecda9a99d6544ec28d90a6915751de259264" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "refinery-core", + "regex", + "syn 2.0.106", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "async-compression", + "base64 0.22.1", + "bytes", + "cookie 0.18.1", + "cookie_store", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-rustls 0.27.7", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.31", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.26.2", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.2", +] + +[[package]] +name = "reqwest-middleware" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +dependencies = [ + "anyhow", + "async-trait", + "http 1.3.1", + "reqwest", + "serde", + "thiserror 1.0.69", + "tower-service", +] + +[[package]] +name = "reqwest-ratelimit" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8fff0d8036f23dcad6c27605ca3baa8ae3867438d0a8b34072f40f6c8bf628" +dependencies = [ + "async-trait", + "http 1.3.1", + "reqwest", + "reqwest-middleware", +] + +[[package]] +name = "reqwest-retry" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c73e4195a6bfbcb174b790d9b3407ab90646976c55de58a6515da25d851178" +dependencies = [ + "anyhow", + "async-trait", + "futures", + "getrandom 0.2.16", + "http 1.3.1", + "hyper 1.7.0", + "parking_lot 0.11.2", + "reqwest", + "reqwest-middleware", + "retry-policies", + "thiserror 1.0.69", + "tokio", + "tracing", + "wasm-timer", +] + +[[package]] +name = "reqwest-websocket" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd5f79b25f7f17a62cc9337108974431a66ae5a723ac0d9fe78ac1cce2027720" +dependencies = [ + "async-tungstenite", + "bytes", + "futures-util", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.16", + "tokio", + "tokio-util", + "tracing", + "tungstenite", + "web-sys", +] + +[[package]] +name = "resize-slice" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3cb2f74a9891e76958b9e0ccd269a25b466c3ae3bb3efd71db157248308c4a" +dependencies = [ + "uninitialized", +] + +[[package]] +name = "retry-policies" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.9.3", + "serde", + "serde_derive", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.3", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.3", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.103.4", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.3.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "aws-lc-rs", + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.3", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags 2.9.3", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34836a629bcbc6f1afdf0907a744870039b1e14c0561cb26094fa683b158eff3" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.11.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time 0.3.42", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.16", + "time 0.3.42", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "size" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b" +dependencies = [ + "serde", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.106", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.3", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tabled" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121d8171ee5687a4978d1b244f7d99c43e7385a272185a2f1e1fa4dc0979d444" +dependencies = [ + "papergrid", + "tabled_derive", +] + +[[package]] +name = "tabled_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d9946811baad81710ec921809e2af67ad77719418673b2a3794932d57b7538" +dependencies = [ + "heck", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tanu" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa0a72002d2dd371f8e042c01cc132e90960960a9a032182f71a4f2bea9bbe66" +dependencies = [ + "anyhow", + "async-trait", + "clap", + "color-eyre", + "console", + "eyre", + "futures", + "itertools 0.13.0", + "log", + "num_cpus", + "once_cell", + "pretty_assertions", + "serde", + "strum 0.26.3", + "tanu-core", + "tanu-derive", + "tanu-tui", + "thiserror 1.0.69", + "tokio", + "toml 0.8.23", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tanu-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca865e4ede20d65cdcea4979a3a4670d2372b0b618bcfa950c25c69ba59fadd" +dependencies = [ + "anyhow", + "async-trait", + "backon", + "chrono", + "console", + "dotenv", + "eyre", + "futures", + "http 1.3.1", + "humantime-serde", + "indexmap 2.11.0", + "itertools 0.13.0", + "once_cell", + "pretty_assertions", + "reqwest", + "serde", + "serde_json", + "strum 0.26.3", + "tabled", + "tanu-derive", + "thiserror 2.0.16", + "tokio", + "toml 0.8.23", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "tanu-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d4fa711d3072ea6af7088742ea448ab9fcfed315df126f878cba114d208fd39" +dependencies = [ + "eyre", + "itertools 0.13.0", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.106", + "walkdir", +] + +[[package]] +name = "tanu-tui" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46ca2135608d3d5690f181a42293de7251e0b4385493d20c666d5e9fe66be79f" +dependencies = [ + "ansi-to-tui", + "async-trait", + "crossterm", + "dotenv", + "eyre", + "futures", + "http 1.3.1", + "itertools 0.13.0", + "log", + "memoize", + "once_cell", + "ratatui", + "serde_json", + "strum 0.26.3", + "syntect", + "tanu-core", + "textwrap", + "throbber-widgets-tui", + "tokio", + "tracing", + "tracing-log", + "tracing-subscriber", + "tui-big-text", + "tui-logger", +] + +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.8", + "windows-sys 0.60.2", +] + +[[package]] +name = "tests" +version = "0.1.0" +dependencies = [ + "futures", + "hex", + "hulyrs", + "rand 0.9.2", + "reqwest", + "secrecy", + "serde_json", + "tanu", + "tokio", + "uuid", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width 0.2.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "throbber-widgets-tui" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d36b5738d666a2b4c91b7c24998a8588db724b3107258343ebf8824bf55b06d" +dependencies = [ + "rand 0.8.5", + "ratatui", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca967379f9d8eb8058d86ed467d81d03e81acd45757e4ca341c24affbe8e8e3" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9108bb380861b07264b950ded55a44a14a4adc68b9f5efd85aafc3aa4d40a68" + +[[package]] +name = "time-macros" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7182799245a7264ce590b349d90338f1c1affad93d2639aed5f8f69c090b334c" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot 0.12.4", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot 0.12.4", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.9.2", + "socket2 0.5.10", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls 0.23.31", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio_with_wasm" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dfba9b946459940fb564dcf576631074cdfb0bfe4c962acd4c31f0dca7897e6" +dependencies = [ + "js-sys", + "tokio", + "tokio_with_wasm_proc", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "tokio_with_wasm_proc" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e04c1865c281139e5ccf633cb9f76ffdaabeebfe53b703984cf82878e2aabb" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + +[[package]] +name = "toml" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +dependencies = [ + "serde", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.11.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tonic" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project", + "prost", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.3", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-actix-web" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5360edd490ec8dee9fedfc6a9fd83ac2f01b3e1996e3261b9ad18a61971fe064" +dependencies = [ + "actix-web", + "mutually_exclusive_features", + "pin-project", + "tracing", + "uuid", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tui-big-text" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97cefa9f1425ab6146db2961241cec86845d11105b5dd6bb504294b0cdd21af" +dependencies = [ + "derive_builder", + "font8x8", + "itertools 0.14.0", + "ratatui", +] + +[[package]] +name = "tui-logger" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3fb54e48fa37d5081603f7804c730734b381235ebd876cb1e66f853a7d2533" +dependencies = [ + "chrono", + "fxhash", + "lazy_static", + "log", + "parking_lot 0.12.4", + "ratatui", + "tracing", + "tracing-subscriber", + "unicode-segmentation", +] + +[[package]] +name = "tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +dependencies = [ + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.16", + "utf-8", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uninitialized" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c1aa4511c38276c548406f0b1f5f8b793f000cfb51e18f278a102abd057e81" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.3+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmtimer" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d49b5d6c64e8558d9b1b065014426f35c18de636895d24893dbbd329743446" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.12.4", + "pin-utils", + "slab", + "wasm-bindgen", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link 0.1.3", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yaml-rust2" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce2a4ff45552406d02501cea6c18d8a7e50228e7736a872951fe2fe75c91be7" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/foundations/hulylake/Cargo.toml b/foundations/hulylake/Cargo.toml new file mode 100644 index 0000000000..71b23b6b49 --- /dev/null +++ b/foundations/hulylake/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "3" +members = ["server", "tests"] diff --git a/foundations/hulylake/Dockerfile b/foundations/hulylake/Dockerfile new file mode 100644 index 0000000000..0227824801 --- /dev/null +++ b/foundations/hulylake/Dockerfile @@ -0,0 +1,39 @@ +FROM --platform=$BUILDPLATFORM rust:1.88 AS builder +ARG TARGETPLATFORM + +WORKDIR /tmp/build + +COPY . . + +RUN echo build for $TARGETPLATFORM +RUN \ + if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ + apt-get update && apt-get install -y cmake \ + && rm -rf /var/lib/apt/lists/* ; \ + cargo build --release --target=x86_64-unknown-linux-gnu --package hulylake; \ + elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ + apt-get update && apt-get install -y \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu \ + libc6-dev-arm64-cross \ + cmake \ + && rm -rf /var/lib/apt/lists/* ; \ + rustup target add aarch64-unknown-linux-gnu ; \ + export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc ; \ + export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ ; \ + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc ; \ + cargo tree --target=aarch64-unknown-linux-gnu ; \ + cargo build --release --target=aarch64-unknown-linux-gnu --package hulylake ; \ + else \ + echo "Unexpected target platform: $TARGETPLATFORM" && exit 1 ; \ + fi + +RUN cargo test + +FROM debian:12-slim + +ARG TARGET +COPY --from=builder /tmp/build/target/*/release/hulylake /usr/local/bin/hulylake +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +ENTRYPOINT ["/usr/local/bin/hulylake"] diff --git a/foundations/hulylake/Justfile b/foundations/hulylake/Justfile new file mode 100644 index 0000000000..8648ef0693 --- /dev/null +++ b/foundations/hulylake/Justfile @@ -0,0 +1,2 @@ +image platform='linux/amd64': + cd server && docker buildx build --tag=hardcoreeng/hulylake:latest --platform={{platform}} --load . \ No newline at end of file diff --git a/foundations/hulylake/server/Cargo.toml b/foundations/hulylake/server/Cargo.toml new file mode 100644 index 0000000000..ef9fbaa554 --- /dev/null +++ b/foundations/hulylake/server/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "hulylake" +version = "0.1.18" +edition = "2024" +rust-version = "1.88.0" + +[dependencies] +tokio = { version = "1", features = ["full"] } +tracing = "0.1.41" +tracing-subscriber = "0.3.19" +anyhow = "1.0.99" +config = "0.15.14" +serde = "1.0.219" +actix-web = "4.11.0" +actix-cors = "0.7.1" +refinery = { version = "0.8.16", features = ["tokio-postgres"] } +tokio-postgres = { version = "0.7.13", features = [ + "with-uuid-1", + "with-serde_json-1", + "with-chrono-0_4", +] } +bb8 = "0.9.0" +bb8-postgres = { version = "0.9.0", features = ["with-uuid-1"] } +md5 = "0.8.0" +size = { version = "0.5.0", features = ["serde"] } +uuid = { version = "1.18", features = ["v4", "serde"] } +serde_json = "1.0" +hulyrs = { git = "https://github.com/hcengineering/hulyrs.git", features = ["actix", "otel"] } +secrecy = "0.10.3" +tracing-actix-web = "0.7.19" +aws-config = { version = "1.8.5" } +aws-sdk-s3 = "1.103.0" +futures-util = "0.3.31" +tokio-stream = "0.1.17" +thiserror = "2.0.16" +bytes = "1.10.1" +ksuid = "0.2.0" +tracing-opentelemetry = "0.31.0" +mime = "0.3.17" +async-stream = "0.3.6" +blake3 = "1.8.2" +futures = "0.3.31" +strum = { version = "0.27.2", features = ["derive"] } +json-patch = "4.0.0" +jsonptr = "0.7.1" +chrono = { version = "0.4.42", features = ["now"] } +opentelemetry = "0.30.0" +opentelemetry-appender-tracing = "0.30.1" +lockable = "0.2.0" diff --git a/foundations/hulylake/server/Justfile b/foundations/hulylake/server/Justfile new file mode 100644 index 0000000000..15818dcee4 --- /dev/null +++ b/foundations/hulylake/server/Justfile @@ -0,0 +1,11 @@ +workspace := "4cd5a9d5-7c74-47b1-ac93-265f0bcc73af" +key := "abcd" +headers := '-H "Content-Type:" -H "huly-header-x-test: test" -H "huly-meta-x-test: test"' +token := `cat _hidden/token.txt` + + +put: + curl -X PUT -i -H "Authorization: Bearer {{token}}" -H @_hidden/headers.txt --data-binary @_hidden/data_large.bin http://localhost:8096/api/{{workspace}}/{{key}} + +get: + curl -X GET -s -D - -H "Authorization: Bearer {{token}}" -H @_hidden/headers.txt -o _hidden/data_output.bin http://localhost:8096/api/{{workspace}}/{{key}} diff --git a/foundations/hulylake/server/etc/migrations/V1__initial.sql b/foundations/hulylake/server/etc/migrations/V1__initial.sql new file mode 100644 index 0000000000..cd4f8c1f73 --- /dev/null +++ b/foundations/hulylake/server/etc/migrations/V1__initial.sql @@ -0,0 +1,18 @@ +create table blob( + key text not null, + hash text not null +); + +create unique index blob_key on blob(key); +create unique index blob_hash on blob(hash); + + +create table object( + workspace uuid not null, + key text not null, + part int4 not null, + data jsonb not null, + inline bytea, + + primary key (workspace, key, part) +) \ No newline at end of file diff --git a/foundations/hulylake/server/src/blob.rs b/foundations/hulylake/server/src/blob.rs new file mode 100644 index 0000000000..772377dd8e --- /dev/null +++ b/foundations/hulylake/server/src/blob.rs @@ -0,0 +1,198 @@ +use std::error::Error as StdError; + +use aws_sdk_s3::primitives::ByteStream; +use blake3::Hasher; +use bytes::{Bytes, BytesMut}; +use futures::stream::StreamExt; +use futures_util::Stream; +use size::Size; +use tokio_postgres::error::SqlState; +use tracing::*; + +use crate::handlers::ApiError; +use crate::postgres::DbError; +use crate::recovery; +use crate::s3::S3Client; +use crate::{ + config::CONFIG, + postgres::{self, Pool}, +}; + +#[derive(Debug)] +pub struct Blob { + pub s3_key: String, + pub hash: String, + pub length: usize, + pub inline: Option, + pub parts_count: Option, + pub deduplicated: bool, +} + +fn random_key() -> String { + ksuid::Ksuid::generate().to_base62() +} + +#[instrument(level = "debug", skip_all, fields(s3_bucket))] +pub async fn upload( + s3: &S3Client, + pool: &Pool, + length: Size, + mut source: S, +) -> Result +where + S: Stream> + Unpin, + E: StdError + Send + Sync + 'static, +{ + let span = Span::current(); + + let s3_bucket = &CONFIG.s3_bucket; + span.record("s3_bucket", &s3_bucket); + + let blob = if length < CONFIG.multipart_threshold { + let mut hash = Hasher::new(); + + let mut buffer = BytesMut::new(); + + // read in all chunks, but not more that length + while let Some(Ok(chunk)) = source.next().await { + buffer.extend_from_slice(&chunk); + + if buffer.len() > length.bytes() as usize { + return Err(actix_web::error::ErrorPayloadTooLarge("payload too large").into()); + } + } + + if buffer.len() != length.bytes() as usize { + return Err(actix_web::error::ErrorBadRequest("payload size mismatch").into()); + } + + let buffer = buffer.freeze(); + + let hash = hash.update(&buffer).finalize().to_hex().to_string(); + let length = buffer.len(); + + let inline = Some(buffer.clone()); + + let (s3_key, deduplicated) = + if let Some(s3_key_found) = postgres::find_blob_by_hash(&pool, &hash).await? { + span.record("s3_key", &s3_key_found); + debug!(s3_key_found, "blob deduplicated"); + (s3_key_found, true) + } else { + let s3_key = random_key(); + span.record("s3_key", &s3_key); + + s3.put_object() + .bucket(s3_bucket) + .key(&s3_key) + .body(ByteStream::from(buffer)) + .send() + .await?; + + match make_blob(pool, &s3_key, &hash).await? { + Some(s3_key_found) => { + debug!(s3_key_found, "blob deduplicated"); + + // delete uploaded + s3.delete_object() + .bucket(s3_bucket) + .key(&s3_key) + .send() + .await?; + + (s3_key_found, true) + } + None => { + debug!("blob created"); + (s3_key, false) + } + } + }; + + Blob { + hash, + s3_key, + length, + inline, + parts_count: None, + deduplicated, + } + } else { + let s3_key = random_key(); + span.record("s3_key", &s3_key); + + let upload = crate::s3::multipart_upload(&s3, &s3_bucket, &s3_key, source).await?; + + let hash = upload.hash.to_hex().to_string(); + + let (s3_key, deduplicated) = match postgres::find_blob_by_hash(&pool, &hash).await? { + Some(s3_key_found) => { + debug!(s3_key_found, "blob deduplicated"); + (s3_key_found, true) + } + None => { + match make_blob(pool, &s3_key, &hash).await? { + Some(s3_key_found) => { + debug!(s3_key_found, "blob deduplicated"); + + // delete uploaded + s3.delete_object() + .bucket(s3_bucket) + .key(&s3_key) + .send() + .await?; + + (s3_key_found, true) + } + None => { + debug!("blob created"); + (s3_key, false) + } + } + } + }; + + Blob { + hash, + s3_key, + length: upload.length, + inline: None, + parts_count: Some(upload.parts_count), + deduplicated, + } + }; + + if !blob.deduplicated { + recovery::set_blob(&s3, &blob.s3_key, &blob.hash).await?; + } + + Ok(blob) +} + +async fn make_blob(pool: &Pool, s3_key: &String, hash: &str) -> Result, DbError> { + let mut retries = 3; + + loop { + match postgres::insert_blob(&pool, &s3_key, &hash).await { + Ok(_) => break Ok(None), + Err(e) => { + if matches!(e, DbError::Db(ref db_err) if db_err.code() == Some(&SqlState::UNIQUE_VIOLATION)) + { + debug!("concurrent upload detected"); + + if let Some(s3_key_found) = postgres::find_blob_by_hash(&pool, &hash).await? { + break Ok(Some(s3_key_found)); + } + + if retries > 0 { + retries -= 1; + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + continue; + } + } + + break Err(e); + } + }; + } +} diff --git a/foundations/hulylake/server/src/compact.rs b/foundations/hulylake/server/src/compact.rs new file mode 100644 index 0000000000..ec4bb5c461 --- /dev/null +++ b/foundations/hulylake/server/src/compact.rs @@ -0,0 +1,187 @@ +use std::collections::HashSet; +use std::sync::Arc; + +use size::Size; +use tokio::sync::{RwLock, mpsc}; +use tracing::*; +use uuid::Uuid; + +use crate::config::CONFIG; +use crate::handlers::{ApiError, PartData}; +use crate::merge; +use crate::mutex::KeyMutex; +use crate::postgres::{ObjectPart, Pool}; +use crate::s3::S3Client; +use crate::{blob, postgres, recovery}; + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct CompactTask { + pub workspace: Uuid, + pub key: String, +} + +pub struct CompactWorker { + ingest_tx: mpsc::Sender, + ingest_handle: tokio::task::JoinHandle<()>, + compact_handle: tokio::task::JoinHandle<()>, +} + +impl CompactWorker { + pub fn new(s3: Arc, pool: Pool, lock: KeyMutex, buffer_size: usize) -> Self { + let (ingest_tx, ingest_rx) = mpsc::channel(buffer_size); + let (compact_tx, compact_rx) = mpsc::channel(buffer_size); + + let pending_tasks = Arc::new(RwLock::new(HashSet::new())); + let pending_tasks_ingest = pending_tasks.clone(); + let pending_tasks_compact = pending_tasks.clone(); + + let ingest_handle = tokio::spawn(async move { + debug!(buffer_size, "started ingest worker"); + Self::run_ingest_worker(ingest_rx, compact_tx, pending_tasks_ingest).await + }); + + let compact_handle = tokio::spawn(async move { + debug!(buffer_size, "started compact worker"); + Self::run_compact_worker( + compact_rx, + s3.clone(), + pool, + lock.clone(), + pending_tasks_compact, + ) + .await; + }); + + Self { + ingest_tx, + ingest_handle, + compact_handle, + } + } + + async fn run_ingest_worker( + mut ingest_rx: mpsc::Receiver, + compact_tx: mpsc::Sender, + pending_tasks: Arc>>, + ) { + loop { + while let Some(task) = ingest_rx.recv().await { + let is_new = pending_tasks.write().await.insert(task.clone()); + if !is_new { + continue; + } + + if let Err(err) = compact_tx.send(task.clone()).await { + error!(%err, "failed to send compact task"); + pending_tasks.write().await.remove(&task); + } + } + } + } + + async fn run_compact_worker( + mut rx: mpsc::Receiver, + s3: Arc, + pool: Pool, + lock: KeyMutex, + pending_tasks: Arc>>, + ) { + loop { + while let Some(task) = rx.recv().await { + let CompactTask { workspace, key } = task.clone(); + + let _guard = lock.lock(workspace, key).await; + + pending_tasks.write().await.remove(&task); + + let res = compact(s3.clone(), pool.clone(), task.clone()).await; + match res { + Ok(_) => debug!(workspace = %task.workspace, key = %task.key, "blob compacted"), + Err(err) => error!(%err, "failed to compact"), + } + } + } + } + + pub async fn try_send(&self, parts: &Vec>) -> bool { + if parts.len() >= CONFIG.compact_parts_limit { + let task = CompactTask { + workspace: parts[0].data.workspace, + key: parts[0].data.key.clone(), + }; + self.send(task).await + } else { + false + } + } + + pub async fn send(&self, task: CompactTask) -> bool { + let res = self.ingest_tx.send(task).await; + match res { + Ok(_) => true, + Err(err) => { + warn!(%err, "failed to schedule compact"); + false + } + } + } + + pub async fn stop(&self) { + self.ingest_handle.abort(); + self.compact_handle.abort(); + } +} + +#[instrument(level = "debug", skip_all, fields(workspace, huly_key))] +async fn compact(s3: Arc, pool: Pool, task: CompactTask) -> anyhow::Result<(), ApiError> { + let pool = pool.clone(); + + let workspace = task.workspace; + let key = task.key; + + Span::current() + .record("workspace", workspace.to_string()) + .record("huly_key", &key); + + let parts = postgres::find_parts(&pool, task.workspace, &key).await?; + let first = &parts.first().unwrap().data; + let last = &parts.last().unwrap().data; + + let stream = merge::stream(s3.clone(), parts.to_vec()).await?; + + let uploaded = blob::upload( + &s3, + &pool, + Size::from_bytes(stream.content_length), + stream.stream, + ) + .await?; + + let inline = uploaded.inline.and_then(|inline| { + if inline.len() < CONFIG.inline_threshold.bytes() as usize { + Some(inline) + } else { + None + } + }); + + let part_data = PartData { + workspace, + key: key.to_owned(), + part: 0, + blob: uploaded.s3_key, + size: uploaded.length, + etag: last.etag.clone(), + date: last.date.clone(), + + headers: first.headers.clone(), + meta: first.meta.clone(), + merge_strategy: first.merge_strategy, + }; + let obj_parts = vec![&part_data]; + + postgres::set_part(&pool, workspace, &key, inline, &part_data).await?; + recovery::set_object(&s3, workspace, &key, obj_parts, None).await?; + + Ok(()) +} diff --git a/foundations/hulylake/server/src/conditional.rs b/foundations/hulylake/server/src/conditional.rs new file mode 100644 index 0000000000..0f73e446e4 --- /dev/null +++ b/foundations/hulylake/server/src/conditional.rs @@ -0,0 +1,167 @@ +use actix_web::HttpRequest; +use actix_web::http::header::EntityTag; +use actix_web::http::header::Header; +use actix_web::http::header::IfMatch; +use actix_web::http::header::IfNoneMatch; + +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum ConditionalError { + #[error("Invalid header")] + ParseError, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ConditionalMatch { + IfMatch(String), + IfNoneMatch(String), +} + +pub fn any_match( + req: &HttpRequest, + etag: Option, +) -> Result, ConditionalError> { + match IfMatch::parse(req) { + Ok(IfMatch::Any) => Ok(Some(etag.is_some())), + Ok(IfMatch::Items(items)) => Ok({ + if items.is_empty() { + None + } else { + Some(etag.map_or(false, |e| items.contains(&e))) + } + }), + Err(_) => Err(ConditionalError::ParseError), + } +} + +pub fn none_match( + req: &HttpRequest, + etag: Option, +) -> Result, ConditionalError> { + match IfNoneMatch::parse(req) { + Ok(IfNoneMatch::Any) => Ok(Some(etag.is_none())), + Ok(IfNoneMatch::Items(items)) => Ok({ + if items.is_empty() { + None + } else { + Some(etag.map_or(true, |e| !items.contains(&e))) + } + }), + Err(_) => Err(ConditionalError::ParseError), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use actix_web::test::TestRequest; + + #[test] + fn test_any_match_none_some() { + assert_eq!( + any_match( + &TestRequest::default().to_http_request(), + Some(EntityTag::new_strong("foo".to_owned())) + ), + Ok(None) + ); + } + + #[test] + fn test_any_match_any_some() { + assert_eq!( + any_match( + &TestRequest::default() + .insert_header(("If-Match", "*")) + .to_http_request(), + Some(EntityTag::new_strong("foo".to_owned())) + ), + Ok(Some(true)) + ); + } + + #[test] + fn test_any_match_some_some() { + assert_eq!( + any_match( + &TestRequest::default() + .insert_header(("If-Match", "\"foo\" ,\"bar\"")) + .to_http_request(), + Some(EntityTag::new_strong("foo".to_owned())) + ), + Ok(Some(true)) + ); + } + + #[test] + fn test_none_match_none_none() { + assert_eq!( + none_match(&TestRequest::default().to_http_request(), None), + Ok(None) + ); + } + + #[test] + fn test_none_match_any_some() { + assert_eq!( + none_match( + &TestRequest::default() + .insert_header(("If-None-Match", "*")) + .to_http_request(), + Some(EntityTag::new_strong("foo".to_owned())) + ), + Ok(Some(false)) + ); + } + + #[test] + fn test_none_match_any_none() { + assert_eq!( + none_match( + &TestRequest::default() + .insert_header(("If-None-Match", "*")) + .to_http_request(), + None + ), + Ok(Some(true)) + ); + } + + #[test] + fn test_none_match_some_some() { + assert_eq!( + none_match( + &TestRequest::default() + .insert_header(("If-None-Match", "\"foo\"")) + .to_http_request(), + Some(EntityTag::new_strong("foo".to_owned())) + ), + Ok(Some(false)) + ); + } + + #[test] + fn test_none_match_some_none() { + assert_eq!( + none_match( + &TestRequest::default() + .insert_header(("If-None-Match", "\"foo\"")) + .to_http_request(), + None + ), + Ok(Some(true)) + ); + } + + #[test] + fn test_none_match_some_unknown() { + assert_eq!( + none_match( + &TestRequest::default() + .insert_header(("If-None-Match", "\"foo\"")) + .to_http_request(), + Some(EntityTag::new_strong("bar".to_owned())) + ), + Ok(Some(true)) + ); + } +} diff --git a/foundations/hulylake/server/src/config.rs b/foundations/hulylake/server/src/config.rs new file mode 100644 index 0000000000..1eee9f3886 --- /dev/null +++ b/foundations/hulylake/server/src/config.rs @@ -0,0 +1,101 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use std::{path::Path, sync::LazyLock}; + +use config::FileFormat; +use secrecy::SecretString; +use serde::Deserialize; +use size::Size; + +#[derive(Deserialize, Debug)] +pub struct Config { + pub bind_port: u16, + pub bind_host: String, + + pub token_secret: SecretString, + + pub db_connection: String, + pub db_scheme: String, + + pub s3_bucket: String, + + // use multipart upload if blob size is greater than this + pub multipart_threshold: Size, + + // store blobs inline if size is less than this + pub inline_threshold: Size, + + pub cache_control: String, + + pub compact_parts_limit: usize, + pub compact_buffer_size: usize, +} + +pub mod hulyrs { + use std::sync::LazyLock; + + pub static CONFIG: LazyLock = LazyLock::new(|| match hulyrs::Config::auto() { + Ok(config) => config, + Err(error) => { + eprintln!("configuration error: {}", error); + std::process::exit(1); + } + }); +} + +pub static CONFIG: LazyLock = LazyLock::new(|| { + const DEFAULTS: &str = r#" + bind_port = 8096 + bind_host = "0.0.0.0" + + token_secret = "secret" + + db_connection = "postgresql://root@huly.local:26257/defaultdb?sslmode=disable" + db_scheme = "hulylake" + + s3_bucket = "hulylake" + + multipart_threshold = "4MB" + inline_threshold = "100KB" + + cache_control = "public, no-cache" + + compact_parts_limit = 100 + compact_buffer_size = 1000 + "#; + + let mut builder = + config::Config::builder().add_source(config::File::from_str(DEFAULTS, FileFormat::Toml)); + + let path = Path::new("etc/config.toml"); + + if path.exists() { + builder = builder.add_source(config::File::with_name(path.as_os_str().to_str().unwrap())); + } + + let settings = builder + .add_source(config::Environment::with_prefix("HULY")) + .build() + .and_then(|c| c.try_deserialize::()); + + match settings { + Ok(settings) => settings, + Err(error) => { + eprintln!("configuration error: {}", error); + std::process::exit(1); + } + } +}); diff --git a/foundations/hulylake/server/src/handlers.rs b/foundations/hulylake/server/src/handlers.rs new file mode 100644 index 0000000000..207a58116e --- /dev/null +++ b/foundations/hulylake/server/src/handlers.rs @@ -0,0 +1,772 @@ +use std::{collections::HashMap, fmt::Display, io, str::FromStr, time::SystemTime}; + +use actix_web::{ + HttpRequest, HttpResponse, + body::SizedStream, + dev::ServiceRequest, + http::{ + self, StatusCode, + header::{self, ContentLength, ContentType, EntityTag, HttpDate, Range}, + }, + web::{Data, Header, Path, Payload}, +}; +use aws_sdk_s3::error::SdkError; +use chrono::{DateTime, Utc}; +use futures::{StreamExt, stream}; +use serde::{Deserialize, Serialize}; +use size::Size; +use tracing::*; +use uuid::Uuid; + +use crate::s3::S3Client; +use crate::{ + blob, + conditional::{ConditionalMatch, any_match, none_match}, + merge, + postgres::ObjectPart, +}; +use crate::{compact::CompactWorker, conditional}; +use crate::{ + config::CONFIG, + postgres::{self, Pool}, +}; +use crate::{merge::MergeStrategy, recovery}; + +#[derive(Deserialize, Debug)] +pub struct ObjectPath { + pub workspace: Uuid, + pub key: String, +} + +#[derive(thiserror::Error, Debug)] +pub enum ApiError { + #[error("S3 Error: {0}")] + S3(String), + + #[error(transparent)] + Db(#[from] postgres::DbError), + + #[error(transparent)] + ActixError(#[from] actix_web::error::Error), + + #[error(transparent)] + ActixParseError(#[from] actix_web::error::ParseError), + + #[error(transparent)] + ConditionalError(#[from] conditional::ConditionalError), + + #[error("Precondition Failed")] + PreconditionFailed, + + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +pub type HandlerResult = Result; + +impl actix_web::error::ResponseError for ApiError { + fn error_response(&self) -> HttpResponse { + match self { + ApiError::ActixError(error) => error.error_response(), + + ApiError::ConditionalError(_) => HttpResponse::BadRequest().body("Bad Request"), + + ApiError::PreconditionFailed => { + HttpResponse::PreconditionFailed().body("Precondition Failed") + } + + _ => { + tracing::error!(error=?self, "Internal error in http handler"); + HttpResponse::InternalServerError().body("Internal Server Error") + } + } + } +} + +impl From for ApiError { + fn from(error: recovery::RecoveryError) -> Self { + match error { + recovery::RecoveryError::S3(err) => ApiError::S3(err), + recovery::RecoveryError::PreconditionFailed => ApiError::PreconditionFailed, + recovery::RecoveryError::Other(err) => ApiError::Other(err), + } + } +} + +impl From> + for ApiError +{ + fn from(error: SdkError) -> Self { + error.raw_response(); + + ApiError::S3(format!("{} {:#?}", error, error.raw_response())) + } +} + +fn random_etag() -> String { + ksuid::Ksuid::generate().to_base62() +} + +#[derive(Debug, Clone)] +pub struct Headers { + pub content_length: Size, + pub content_type: Option, + pub huly_headers: Vec<(String, String)>, + pub meta: Vec<(String, String)>, +} + +async fn extract_headers(request: &mut ServiceRequest) -> HandlerResult<(Headers, MergeStrategy)> { + let content_length = request + .extract::>() + .await + .map(|header| Size::from_bytes(*header.0)) + .map_err(|_| actix_web::error::ErrorBadRequest("invalid content length"))?; + + let content_type = request + .extract::>() + .await + .map(|header| header.0.to_string()) + .ok(); + + let merge_strategy = request + .headers() + .get("Huly-Merge-Strategy") + .and_then(|v| v.to_str().ok()) + .map(|v| { + MergeStrategy::from_str(v).map_err(|_| { + actix_web::error::ErrorBadRequest(format!("invalid merge strategy: {v}")) + }) + }) + .transpose()? + .unwrap_or_default(); + + let mut huly_headers = Vec::new(); + for (key, value) in request.headers().iter() { + if let Some(header) = key.as_str().strip_prefix("huly-header-") { + if let Ok(value) = value.to_str() { + huly_headers.push((header.to_owned(), value.to_owned())); + } + } + } + if let Some(content_type) = &content_type { + huly_headers.push(( + http::header::CONTENT_TYPE.as_str().to_owned(), + content_type.to_owned(), + )); + } + + let mut meta = Vec::new(); + for (key, value) in request.headers().iter() { + if let Some(header) = key.as_str().strip_prefix("huly-meta-") { + if let Ok(value) = value.to_str() { + meta.push((header.to_owned(), value.to_owned())); + } + } + } + + meta.push(( + "merge-strategy".to_owned(), + serde_json::to_string(&merge_strategy).unwrap(), + )); + + Ok(( + Headers { + content_length, + content_type, + huly_headers, + meta, + }, + merge_strategy, + )) +} + +async fn extract_range_header(request: &mut ServiceRequest) -> Option { + request + .extract::>() + .await + .map(|header| header.0.to_string()) + .ok() +} +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PartData { + pub workspace: Uuid, + pub key: String, + pub part: u32, + pub size: usize, + pub blob: String, + pub etag: String, + + #[serde(default)] + pub date: DateTime, + + #[serde(skip_serializing_if = "Option::is_none")] + pub headers: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub merge_strategy: Option, +} + +#[instrument(level = "debug", skip_all, fields(workspace, huly_key))] +pub async fn put(request: HttpRequest, payload: Payload) -> HandlerResult { + let span = Span::current(); + debug!("put request"); + + let mut request = ServiceRequest::from_request(request); + let path = request.extract::>().await?.into_inner(); + let (headers, merge_strategy) = extract_headers(&mut request).await?; + + merge::validate_put_request(merge_strategy, &headers)?; + + span.record("workspace", path.workspace.to_string()); + span.record("huly_key", &path.key); + + let pool = request.app_data::>().unwrap().to_owned(); + let s3 = request.app_data::>().unwrap().to_owned(); + + let parts = postgres::find_parts::(&pool, path.workspace, &path.key).await?; + + let conditionals = validate_put_conditionals(request.request(), &parts)?; + + let uploaded = blob::upload(&s3, &pool, headers.content_length, payload).await?; + + merge::validate_put_body(merge_strategy, &uploaded)?; + + let part_data = PartData { + workspace: path.workspace, + key: path.key, + part: 0, + blob: uploaded.s3_key, + size: uploaded.length, + etag: random_etag(), + date: chrono::Utc::now(), + + headers: Some(headers.huly_headers.clone().into_iter().collect()), + meta: Some(headers.meta.into_iter().collect()), + merge_strategy: Some(merge_strategy), + }; + + let inline = uploaded.inline.and_then(|inline| { + if inline.len() < CONFIG.inline_threshold.bytes() as usize { + Some(inline) + } else { + None + } + }); + + let obj_parts = vec![&part_data]; + + recovery::set_object(&s3, path.workspace, &part_data.key, obj_parts, conditionals).await?; + + postgres::set_part(&pool, path.workspace, &part_data.key, inline, &part_data).await?; + + let mut response = HttpResponse::Created(); + response.insert_header((header::ETAG, part_data.etag)); + + if uploaded.deduplicated { + response.insert_header(("Huly-Deduplicated", "true")); + } else { + if let Some(parts_count) = uploaded.parts_count { + response.insert_header(("Huly-S3-Parts-Count", parts_count.to_string())); + } + } + + for (key, value) in headers.huly_headers { + response.insert_header((key.as_str(), value.to_owned())); + } + + Ok(response.finish()) +} + +#[instrument(level = "debug", skip_all, fields(workspace, huly_key))] +pub async fn patch(request: HttpRequest, payload: Payload) -> HandlerResult { + let span = Span::current(); + + let mut request = ServiceRequest::from_request(request); + + let path = request.extract::>().await?.into_inner(); + let (headers, _) = extract_headers(&mut request).await?; + + span.record("workspace", path.workspace.to_string()); + span.record("huly_key", &path.key); + + let pool = request.app_data::>().unwrap().to_owned(); + let s3 = request.app_data::>().unwrap().to_owned(); + + let parts = postgres::find_parts::(&pool, path.workspace, &path.key).await?; + + let mut response = if !parts.is_empty() { + let conditionals = validate_patch_conditionals(request.request(), &parts)?; + + let merge_strategy = objectpart_strategy(&parts).unwrap(); + + merge::validate_patch_request(merge_strategy, &headers)?; + + let uploaded = blob::upload(&s3, &pool, headers.content_length, payload).await?; + + merge::validate_patch_body(merge_strategy, &uploaded)?; + + let part = parts + .iter() + .map(|p| p.data.part) + .reduce(u32::max) + .map(|m| m + 1) + .unwrap_or(0); + + let part_data = PartData { + workspace: path.workspace, + key: path.key, + part, + blob: uploaded.s3_key, + size: uploaded.length, + etag: random_etag(), + date: chrono::Utc::now(), + + // defined in the first part + headers: None, + meta: None, + merge_strategy: None, + }; + + let obj_parts = parts + .iter() + .map(|p| &p.data) + .chain(std::iter::once(&part_data)) + .collect::>(); + + recovery::set_object(&s3, path.workspace, &part_data.key, obj_parts, conditionals).await?; + + postgres::append_part( + &pool, + path.workspace, + &part_data.key, + part_data.part, + uploaded.inline, + &part_data, + ) + .await?; + + let mut response = HttpResponse::Created(); + + if uploaded.deduplicated { + response.insert_header(("Huly-Deduplicated", "true")); + } else { + if let Some(parts_count) = uploaded.parts_count { + response.insert_header(("Huly-S3-Parts-Count", parts_count.to_string())); + } + } + + response + } else { + HttpResponse::NotFound() + }; + + Ok(response.finish()) +} + +#[instrument(level = "debug", skip_all, fields(workspace, huly_key))] +pub async fn get(request: HttpRequest) -> HandlerResult { + let span = Span::current(); + + let mut request = ServiceRequest::from_request(request); + + let path = request.extract::>().await?.into_inner(); + + span.record("workspace", path.workspace.to_string()); + span.record("huly_key", &path.key); + + let pool = request.app_data::>().unwrap().to_owned(); + + let parts = postgres::find_parts::(&pool, path.workspace, &path.key).await?; + + let response = if !parts.is_empty() { + let etag = objectpart_etag(&parts).unwrap(); + let date = objectpart_date(&parts).unwrap(); + + let range = extract_range_header(&mut request).await; + + match none_match(request.request(), Some(etag.clone()))? { + Some(false) => HttpResponse::NotModified() + .insert_header((header::ETAG, etag)) + .insert_header((header::LAST_MODIFIED, HttpDate::from(date))) + .insert_header((header::CACHE_CONTROL, CONFIG.cache_control.clone())) + .finish(), + _ => { + let mut response = HttpResponse::Ok(); + + let s3 = request + .app_data::>() + .unwrap() + .to_owned() + .into_inner(); + + let headers = parts[0].data.headers.as_ref(); + if let Some(headers) = headers { + for (header, value) in headers.iter() { + response.insert_header((header.as_str(), value.to_owned())); + } + } + + let accept_ranges = objectpart_accept_ranges(&parts); + if let Some(accept_ranges) = accept_ranges { + response.insert_header((header::ACCEPT_RANGES, accept_ranges)); + } + + response.insert_header((header::ETAG, etag)); + response.insert_header((header::LAST_MODIFIED, HttpDate::from(date))); + response.insert_header((header::CACHE_CONTROL, CONFIG.cache_control.clone())); + + match range { + Some(range) => { + let partial = merge::partial(s3, parts, range).await?; + + if partial.partial { + response.status(StatusCode::PARTIAL_CONTENT); + } + + if let Some(content_range) = partial.content_range { + response.insert_header((header::CONTENT_RANGE, content_range)); + } + + response.body(SizedStream::new(partial.content_length, partial.stream)) + } + None => { + let compact = request.app_data::>().unwrap(); + compact.try_send(&parts).await; + + let stream = merge::stream(s3.clone(), parts).await?; + response.body(SizedStream::new(stream.content_length, stream.stream)) + } + } + } + } + } else { + HttpResponse::NotFound().finish() + }; + + Ok(response) +} + +#[instrument(level = "debug", skip_all, fields(workspace, huly_key))] +pub async fn head(request: HttpRequest) -> HandlerResult { + let span = Span::current(); + + let mut request = ServiceRequest::from_request(request); + + let path = request.extract::>().await?.into_inner(); + + span.record("workspace", path.workspace.to_string()); + span.record("huly_key", &path.key); + + let pool = request.app_data::>().unwrap().to_owned(); + + let parts = postgres::find_parts::(&pool, path.workspace, &path.key).await?; + + let response = if !parts.is_empty() { + let etag = objectpart_etag(&parts).unwrap(); + let date = objectpart_date(&parts).unwrap(); + + match none_match(request.request(), Some(etag.clone()))? { + Some(false) => HttpResponse::NotModified() + .insert_header((header::ETAG, etag)) + .insert_header((header::LAST_MODIFIED, HttpDate::from(date))) + .insert_header((header::CACHE_CONTROL, CONFIG.cache_control.clone())) + .finish(), + _ => { + let mut response = HttpResponse::Ok(); + + let headers = parts[0].data.headers.as_ref(); + if let Some(headers) = headers { + for (header, value) in headers.iter() { + response.insert_header((header.as_str(), value.to_owned())); + } + } + + let accept_ranges = objectpart_accept_ranges(&parts); + if let Some(accept_ranges) = accept_ranges { + response.insert_header((header::ACCEPT_RANGES, accept_ranges)); + } + + response.insert_header((header::ETAG, etag)); + response.insert_header((header::LAST_MODIFIED, HttpDate::from(date))); + response.insert_header((header::CACHE_CONTROL, CONFIG.cache_control.clone())); + + // see https://github.com/actix/examples/blob/master/forms/multipart-s3/src/main.rs#L67-L79 + let content_length = merge::content_length(parts); + match content_length { + Some(content_length) => response.body(SizedStream::new( + content_length as u64, + stream::empty::>().boxed_local(), + )), + None => response.finish(), + } + } + } + } else { + HttpResponse::NotFound().finish() + }; + + Ok(response) +} + +pub async fn delete(_path: Path) -> HandlerResult { + unimplemented!("delete is not implemented") +} + +fn objectpart_etag(parts: &Vec>) -> Option { + parts + .last() + .map(|p| EntityTag::new_strong(p.data.etag.to_owned())) +} + +fn objectpart_date(parts: &Vec>) -> Option { + parts.last().map(|p| p.data.date).map(|d| d.into()) +} + +fn objectpart_strategy(parts: &Vec>) -> Option { + parts.first().map(|p| p.data.merge_strategy.unwrap()) +} + +fn objectpart_accept_ranges(parts: &Vec>) -> Option<&str> { + let strategy = objectpart_strategy(parts)?; + match strategy { + MergeStrategy::JsonPatch => None, + _ => { + if parts.len() == 1 { + Some("bytes") + } else { + None + } + } + } +} + +fn validate_patch_conditionals( + req: &HttpRequest, + parts: &Vec>, +) -> Result, ApiError> { + let etag = objectpart_etag(parts); + + match any_match(req, etag)? { + Some(false) => Err(ApiError::PreconditionFailed), + Some(true) => { + let parts_data = parts.iter().map(|p| &p.data).collect::>(); + let parts_etag = recovery::object_etag(parts_data)?; + + Ok(Some(ConditionalMatch::IfMatch(parts_etag))) + } + None => Ok(None), + } +} + +fn validate_put_conditionals( + req: &HttpRequest, + parts: &Vec>, +) -> Result, ApiError> { + let etag = objectpart_etag(parts); + + match any_match(req, etag.clone())? { + Some(true) => { + let parts_data = parts.iter().map(|p| &p.data).collect::>(); + let parts_etag = recovery::object_etag(parts_data)?; + + Ok(Some(ConditionalMatch::IfMatch(parts_etag))) + } + Some(false) => Err(ApiError::PreconditionFailed), + None => match none_match(req, etag.clone())? { + Some(true) => Ok(Some(ConditionalMatch::IfNoneMatch("*".to_owned()))), + Some(false) => Err(ApiError::PreconditionFailed), + None => Ok(None), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use actix_web::test::TestRequest; + + fn object_part(etag: &str) -> ObjectPart { + ObjectPart { + inline: None, + data: PartData { + workspace: Uuid::new_v4(), + key: "test".to_string(), + part: 0, + size: 0, + blob: "test".to_string(), + etag: etag.to_owned(), + date: Utc::now(), + headers: None, + meta: None, + merge_strategy: None, + }, + } + } + + #[test] + fn test_objectpart_etag() { + let parts = vec![object_part("foo"), object_part("bar")]; + + let etag = objectpart_etag(&parts); + assert_eq!(etag, Some(EntityTag::new_strong("bar".to_owned()))); + } + + #[test] + fn test_validate_patch_conditionals_none_none() { + let req = TestRequest::default().to_http_request(); + let parts = vec![]; + + let res = validate_patch_conditionals(&req, &parts); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), None); + } + + #[test] + fn test_validate_patch_conditionals_none_some() { + let req = TestRequest::default().to_http_request(); + let parts = vec![object_part("foo")]; + + let res = validate_patch_conditionals(&req, &parts); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), None); + } + + #[test] + fn test_validate_patch_conditionals_some_some_match() { + let req = TestRequest::default() + .insert_header((header::IF_MATCH, "\"foo\"")) + .to_http_request(); + let parts = vec![object_part("foo")]; + let etag = recovery::object_etag(vec![&parts[0].data]).unwrap(); + + let res = validate_patch_conditionals(&req, &parts); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), Some(ConditionalMatch::IfMatch(etag))); + } + + #[test] + fn test_validate_patch_conditionals_some_some_not_match() { + let req = TestRequest::default() + .insert_header((header::IF_MATCH, "\"bar\"")) + .to_http_request(); + let parts = vec![object_part("foo")]; + + let res = validate_patch_conditionals(&req, &parts); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + ApiError::PreconditionFailed.to_string() + ); + } + + #[test] + fn test_validate_put_conditionals_none_none() { + let req = TestRequest::default().to_http_request(); + let parts = vec![]; + + let res = validate_put_conditionals(&req, &parts); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), None); + } + + #[test] + fn test_validate_put_conditionals_none_some() { + let req = TestRequest::default().to_http_request(); + let parts = vec![object_part("foo")]; + + let res = validate_put_conditionals(&req, &parts); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), None); + } + + #[test] + fn test_validate_put_conditionals_some_some_match() { + let req = TestRequest::default() + .insert_header((header::IF_MATCH, "\"foo\"")) + .to_http_request(); + let parts = vec![object_part("foo")]; + let etag = recovery::object_etag(vec![&parts[0].data]).unwrap(); + + let res = validate_put_conditionals(&req, &parts); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), Some(ConditionalMatch::IfMatch(etag))); + } + + #[test] + fn test_validate_put_conditionals_some_some_not_match() { + let req = TestRequest::default() + .insert_header((header::IF_MATCH, "\"bar\"")) + .to_http_request(); + let parts = vec![object_part("foo")]; + + let res = validate_put_conditionals(&req, &parts); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + ApiError::PreconditionFailed.to_string() + ); + } + + #[test] + fn test_validate_put_conditionals_if_match_if_none_match() { + let req = TestRequest::default() + .insert_header((header::IF_MATCH, "\"bar\"")) + .insert_header((header::IF_NONE_MATCH, "*")) + .to_http_request(); + let parts = vec![object_part("foo")]; + + let res = validate_put_conditionals(&req, &parts); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + ApiError::PreconditionFailed.to_string() + ); + } + + #[test] + fn test_validate_put_conditionals_if_none_match_match() { + let req = TestRequest::default() + .insert_header((header::IF_NONE_MATCH, "\"bar\"")) + .to_http_request(); + let parts = vec![object_part("foo")]; + + let res = validate_put_conditionals(&req, &parts); + assert!(res.is_ok()); + assert_eq!( + res.unwrap(), + Some(ConditionalMatch::IfNoneMatch("*".to_owned())) + ); + } + + #[test] + fn test_validate_put_conditionals_if_none_match_not_match() { + let req = TestRequest::default() + .insert_header((header::IF_NONE_MATCH, "\"foo\"")) + .to_http_request(); + let parts = vec![object_part("foo")]; + + let res = validate_put_conditionals(&req, &parts); + assert_eq!( + res.unwrap_err().to_string(), + ApiError::PreconditionFailed.to_string() + ); + } + + #[test] + fn test_validate_put_conditionals_if_none_match_err() { + let req = TestRequest::default() + .insert_header((header::IF_NONE_MATCH, "*")) + .to_http_request(); + let parts = vec![object_part("foo")]; + + let res = validate_put_conditionals(&req, &parts); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + ApiError::PreconditionFailed.to_string() + ); + } +} diff --git a/foundations/hulylake/server/src/main.rs b/foundations/hulylake/server/src/main.rs new file mode 100644 index 0000000000..64659e5129 --- /dev/null +++ b/foundations/hulylake/server/src/main.rs @@ -0,0 +1,210 @@ +use std::{net::SocketAddr, sync::Arc}; + +use actix_cors::Cors; +use actix_web::{ + App, Error, HttpMessage, HttpServer, + body::MessageBody, + dev::{ServiceRequest, ServiceResponse}, + middleware::{Next, from_fn}, + web::{self, Data, Path}, +}; +use tracing::*; +use tracing_actix_web::TracingLogger; +use uuid::Uuid; + +use hulyrs::services::jwt::actix::ServiceRequestExt; +use hulyrs::services::otel; + +mod blob; +mod compact; +mod conditional; +mod config; +mod handlers; +mod merge; +mod mutex; +mod patch; +mod postgres; +mod recovery; +mod s3; + +use config::CONFIG; + +use crate::mutex::KeyMutex; + +fn initialize_tracing() { + use opentelemetry::trace::TracerProvider; + use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; + use tracing::Level; + use tracing_opentelemetry::OpenTelemetryLayer; + use tracing_subscriber::{filter::targets::Targets, prelude::*}; + + let otel_config = otel::OtelConfig { + mode: config::hulyrs::CONFIG.otel_mode.clone(), + service_name: env!("CARGO_PKG_NAME").to_string(), + service_version: env!("CARGO_PKG_VERSION").to_string(), + }; + + otel::init(&otel_config); + + let filter = Targets::default() + .with_target(env!("CARGO_BIN_NAME"), config::hulyrs::CONFIG.log) + .with_target("actix", Level::WARN); + let format = tracing_subscriber::fmt::layer().compact(); + + match &config::hulyrs::CONFIG.otel_mode { + otel::OtelMode::Off => { + tracing_subscriber::registry() + .with(filter) + .with(format) + .init(); + } + + _ => { + tracing_subscriber::registry() + .with(filter) + .with(format) + .with(otel::tracer_provider(&otel_config).map(|provider| { + let filter = Targets::default() + .with_default(Level::DEBUG) + .with_target(env!("CARGO_PKG_NAME"), config::hulyrs::CONFIG.log); + + OpenTelemetryLayer::new(provider.tracer("hulylake")).with_filter(filter) + })) + .with(otel::logger_provider(&otel_config).as_ref().map(|logger| { + let filter = Targets::default() + .with_default(Level::DEBUG) + .with_target(env!("CARGO_PKG_NAME"), Level::DEBUG); + + OpenTelemetryTracingBridge::new(logger).with_filter(filter) + })) + .init(); + } + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + initialize_tracing(); + + tracing::info!( + "{}/{} started", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + ); + + tracing::debug!( + db_connection = &CONFIG.db_connection, + db_scheme = &CONFIG.db_scheme, + s3_bucket = &CONFIG.s3_bucket, + "configuration" + ); + + let lock = mutex::KeyMutex::new(); + let postgres = postgres::pool().await?; + let s3 = s3::client().await; + + match s3.head_bucket().bucket(&CONFIG.s3_bucket).send().await { + Ok(_) => info!(bucket = &CONFIG.s3_bucket, "s3 bucket exists and available"), + Err(_) => { + s3.create_bucket().bucket(&CONFIG.s3_bucket).send().await?; + info!(bucket = &CONFIG.s3_bucket, "s3 bucket created"); + } + } + + let bind_to = SocketAddr::new(CONFIG.bind_host.as_str().parse()?, CONFIG.bind_port); + + #[allow(dead_code)] + async fn auth( + mut request: ServiceRequest, + next: Next, + ) -> Result, Error> { + let claims = request + .extract_claims(&CONFIG.token_secret) + .map_err(|error| { + let method = request.method(); + let path = request.path(); + warn!(%method, path, %error, "Unauthorized request"); + error + })?; + + let workspace = Uuid::parse_str(&request.extract::>().await?); + + if claims.is_system() || Ok(claims.workspace.clone()) == workspace.clone().map(Some) { + request.extensions_mut().insert(claims); + next.call(request).await + } else { + warn!( + expected = ?claims.workspace, + actual = ?workspace, + "Unauthorized request, workspace mismatch" + ); + Err(actix_web::error::ErrorUnauthorized("Unauthorized").into()) + } + } + + async fn mutex( + mut request: ServiceRequest, + next: Next, + ) -> Result, Error> { + let path = request + .extract::>() + .await? + .into_inner(); + + let mutex = request.app_data::>().unwrap().to_owned(); + + let _guard = mutex.lock(path.workspace, path.key).await; + + next.call(request).await + } + + let compactor = compact::CompactWorker::new( + Arc::new(s3.clone()), + postgres.clone(), + lock.clone(), + CONFIG.compact_buffer_size, + ); + let compactor_data = Data::new(compactor); + let compactor_handle = compactor_data.clone(); + + let server = HttpServer::new(move || { + let cors = Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header() + .supports_credentials() + .max_age(3600); + + const KEY_PATH: &str = "/{key:.*}"; + + App::new() + .app_data(Data::new(postgres.clone())) + .app_data(Data::new(s3.clone())) + .app_data(Data::new(lock.clone())) + .app_data(compactor_data.clone()) + .wrap(TracingLogger::default()) + .wrap(cors) + .service( + web::scope("/api/{workspace}") + .wrap(from_fn(auth)) + .route(KEY_PATH, web::head().to(handlers::head)) + .route(KEY_PATH, web::get().to(handlers::get)) + .route(KEY_PATH, web::put().to(handlers::put).wrap(from_fn(mutex))) + .route( + KEY_PATH, + web::patch().to(handlers::patch).wrap(from_fn(mutex)), + ) + .route(KEY_PATH, web::delete().to(handlers::delete)), + ) + .route("/status", web::get().to(async || "ok")) + }) + .bind(bind_to)? + .run(); + + info!("http listener on {}", bind_to); + + server.await?; + compactor_handle.stop().await; + + Ok(()) +} diff --git a/foundations/hulylake/server/src/merge.rs b/foundations/hulylake/server/src/merge.rs new file mode 100644 index 0000000000..335ec23290 --- /dev/null +++ b/foundations/hulylake/server/src/merge.rs @@ -0,0 +1,430 @@ +use std::{io::Error as IoError, pin::Pin, sync::Arc}; + +use actix_web::error::ErrorBadRequest; +use async_stream::stream; +use bytes::Bytes; +use futures_util::Stream; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, from_slice}; +use tracing::*; + +use crate::handlers::PartData; +use crate::handlers::{HandlerResult, Headers}; +use crate::patch; +use crate::postgres::ObjectPart; +use crate::s3::S3Client; +use crate::{blob::Blob, config::CONFIG}; + +#[derive( + Clone, Copy, Debug, Serialize, Deserialize, Default, strum::EnumString, strum::Display, +)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum MergeStrategy { + JsonPatch, + + #[default] + Concatenate, +} + +pub fn validate_put_request(merge_strategy: MergeStrategy, headers: &Headers) -> HandlerResult<()> { + match merge_strategy { + MergeStrategy::JsonPatch + if headers.content_type != Some("application/json".to_string()) + || headers.content_length > CONFIG.inline_threshold => + { + Err(ErrorBadRequest("invalid content type and length").into()) + } + + _ => Ok(()), + } +} + +pub fn validate_put_body(merge_strategy: MergeStrategy, blob: &Blob) -> HandlerResult<()> { + match merge_strategy { + MergeStrategy::JsonPatch => match blob.inline.as_ref() { + Some(inline) => { + from_slice::(inline).map_err(|e| ErrorBadRequest(e.to_string()))?; + Ok(()) + } + _ => Err(ErrorBadRequest("missing inline body").into()), + }, + + _ => Ok(()), + } +} + +pub fn validate_patch_request( + merge_strategy: MergeStrategy, + headers: &Headers, +) -> HandlerResult<()> { + match merge_strategy { + MergeStrategy::JsonPatch + if headers.content_type != Some("application/json-patch+json".to_string()) + || headers.content_length > CONFIG.inline_threshold => + { + Err(ErrorBadRequest("invalid content type and length").into()) + } + + _ => Ok(()), + } +} + +pub fn validate_patch_body(merge_strategy: MergeStrategy, blob: &Blob) -> HandlerResult<()> { + match merge_strategy { + MergeStrategy::JsonPatch => match blob.inline.as_ref() { + Some(inline) => { + from_slice::>(inline) + .map_err(|e| ErrorBadRequest(e.to_string()))?; + Ok(()) + } + _ => Err(ErrorBadRequest("missing inline body").into()), + }, + + _ => Ok(()), + } +} + +pub struct PartialResponse { + pub partial: bool, + pub content_range: Option, + pub content_length: u64, + pub stream: Pin>>>, +} + +#[instrument(level = "debug", skip_all)] +pub async fn partial( + s3: Arc, + parts: Vec>, + range: String, +) -> anyhow::Result { + let part = parts.first().unwrap(); + + let mut response = s3 + .get_object() + .bucket(&CONFIG.s3_bucket) + .key(&part.data.blob) + .range(range) + .send() + .await?; + + let content_range = response.content_range().map(|s| s.to_string()); + let content_length = response.content_length().map_or(0, |c| c as u64); + + let stream = stream! { + while let Some(chunk) = response.body.next().await { + yield Ok(Bytes::from(chunk?)); + }; + }; + + Ok(PartialResponse { + partial: part.data.size != content_length as usize, + content_range, + content_length, + stream: Box::pin(stream), + }) +} + +pub struct StreamResponse { + pub content_length: u64, + pub stream: Pin> + Send>>, +} + +#[instrument(level = "debug", skip_all)] +pub async fn stream( + s3: Arc, + parts: Vec>, +) -> anyhow::Result { + let first = parts.first().unwrap(); + let merge_strategy = first.data.merge_strategy.unwrap(); + + match merge_strategy { + MergeStrategy::Concatenate => { + let mut content_length = 0; + + for part in parts.iter() { + content_length += part.data.size; + } + + let stream = stream! { + for parts in parts { + match parts.inline { + Some(inline) => { + yield Ok(Bytes::from(inline)); + }, + None => { + match s3.get_object().bucket(&CONFIG.s3_bucket).key(parts.data.blob).send().await { + Ok(mut response) => { + while let Some(bytes) = response.body.next().await { + yield Ok(bytes?); + } + }, + + Err(error) => { + yield Err(IoError::new(std::io::ErrorKind::Other, error)); + break; + } + } + } + } + } + }; + + Ok(StreamResponse { + content_length: content_length as u64, + stream: Box::pin(stream), + }) + } + + MergeStrategy::JsonPatch => { + let mut acc = None; + + for part in parts { + let part_data = part_data(&s3, part).await?; + + if let Some(acc) = &mut acc { + let ops = serde_json::from_slice::>(&part_data); + match ops { + Ok(ops) => { + if let Err(error) = patch::apply(acc, &ops) { + error!("json patch error: {error}"); + } + } + Err(error) => { + error!("json patch deserialization error: {error}"); + } + } + } else { + acc = Some(serde_json::from_slice::(&part_data)?); + } + } + + let bytes = serde_json::to_vec(&acc.unwrap())?; + let content_length = bytes.len() as u64; + + let stream = stream! { + yield Result::::Ok(Bytes::from(bytes)); + }; + + Ok(StreamResponse { + content_length, + stream: Box::pin(stream), + }) + } + } +} + +pub fn content_length(parts: Vec>) -> Option { + let first = parts.first().unwrap(); + let merge_strategy = first.data.merge_strategy.unwrap(); + + match merge_strategy { + MergeStrategy::Concatenate => { + let mut content_length = 0; + + for part in parts { + content_length += part.data.size; + } + + Some(content_length) + } + + MergeStrategy::JsonPatch => None, + } +} + +async fn part_data(s3: &S3Client, part: ObjectPart) -> anyhow::Result> { + match part.inline { + Some(inline) => Ok(inline), + + None => { + let response = s3 + .get_object() + .bucket(&CONFIG.s3_bucket) + .key(&part.data.blob) + .send() + .await?; + + let body = response.body.collect().await?; + let bytes = body.into_bytes().to_vec(); + + Ok(bytes) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use size::Size; + + #[test] + fn test_validate_put_request() { + let test_cases: Vec<(MergeStrategy, &str, Size, HandlerResult<()>)> = vec![ + ( + MergeStrategy::JsonPatch, + "application/json", + Size::from_bytes(100), + Ok(()), + ), + ( + MergeStrategy::JsonPatch, + "application/json", + Size::from_kb(10), + Ok(()), + ), + ( + MergeStrategy::JsonPatch, + "application/json", + Size::from_mb(1), + Err(ErrorBadRequest("invalid content type and length").into()), + ), + ( + MergeStrategy::JsonPatch, + "text/plain", + Size::from_kb(10), + Err(ErrorBadRequest("invalid content type and length").into()), + ), + ( + MergeStrategy::Concatenate, + "text/plain", + Size::from_mb(1), + Ok(()), + ), + ]; + + for (merge_strategy, content_type, content_length, expected) in test_cases { + let headers = Headers { + content_length, + content_type: Some(content_type.to_string()), + huly_headers: Vec::new(), + meta: Vec::new(), + }; + let res = validate_put_request(merge_strategy, &headers); + match expected { + Ok(_) => assert!(res.is_ok(), "Expected Ok, got Err: {:?}", res.err()), + Err(e) => assert_eq!(res.unwrap_err().to_string(), e.to_string()), + } + } + } + + #[test] + fn test_validate_put_body() { + let test_cases: Vec<(MergeStrategy, Option, HandlerResult<()>)> = vec![ + ( + MergeStrategy::JsonPatch, + Some(Bytes::from( + r#"{ "op": "add", "path": "/foo", "value": "bar" }"#, + )), + Ok(()), + ), + ( + MergeStrategy::JsonPatch, + None, + Err(ErrorBadRequest("missing inline body").into()), + ), + (MergeStrategy::Concatenate, None, Ok(())), + ]; + + for (merge_strategy, body, expected) in test_cases { + let blob = Blob { + hash: "hash".to_string(), + s3_key: "key".to_string(), + length: body.as_ref().map(|b| b.len()).unwrap_or(0), + inline: body, + parts_count: None, + deduplicated: false, + }; + let res = validate_put_body(merge_strategy, &blob); + match expected { + Ok(_) => assert!(res.is_ok(), "Expected Ok, got Err: {:?}", res.err()), + Err(e) => assert_eq!(res.unwrap_err().to_string(), e.to_string()), + } + } + } + + #[test] + fn test_validate_patch_request() { + let test_cases: Vec<(MergeStrategy, &str, Size, HandlerResult<()>)> = vec![ + ( + MergeStrategy::JsonPatch, + "application/json-patch+json", + Size::from_bytes(100), + Ok(()), + ), + ( + MergeStrategy::JsonPatch, + "application/json-patch+json", + Size::from_kb(10), + Ok(()), + ), + ( + MergeStrategy::JsonPatch, + "application/json-patch+json", + Size::from_mb(1), + Err(ErrorBadRequest("invalid content type and length").into()), + ), + ( + MergeStrategy::JsonPatch, + "application/json", + Size::from_kb(10), + Err(ErrorBadRequest("invalid content type and length").into()), + ), + ( + MergeStrategy::Concatenate, + "text/plain", + Size::from_mb(1), + Ok(()), + ), + ]; + + for (merge_strategy, content_type, content_length, expected) in test_cases { + let headers = Headers { + content_length, + content_type: Some(content_type.to_string()), + huly_headers: Vec::new(), + meta: Vec::new(), + }; + let res = validate_patch_request(merge_strategy, &headers); + match expected { + Ok(_) => assert!(res.is_ok(), "Expected Ok, got Err: {:?}", res.err()), + Err(e) => assert_eq!(res.unwrap_err().to_string(), e.to_string()), + } + } + } + + #[test] + fn test_validate_patch_body() { + let test_cases: Vec<(MergeStrategy, Option, HandlerResult<()>)> = vec![ + ( + MergeStrategy::JsonPatch, + Some(Bytes::from( + r#"[{ "op": "add", "path": "/foo", "value": "bar" }]"#, + )), + Ok(()), + ), + ( + MergeStrategy::JsonPatch, + None, + Err(ErrorBadRequest("missing inline body").into()), + ), + (MergeStrategy::Concatenate, None, Ok(())), + ]; + + for (merge_strategy, body, expected) in test_cases { + let blob = Blob { + hash: "hash".to_string(), + s3_key: "key".to_string(), + length: body.as_ref().map(|b| b.len()).unwrap_or(0), + inline: body, + parts_count: None, + deduplicated: false, + }; + let res = validate_patch_body(merge_strategy, &blob); + match expected { + Ok(_) => assert!(res.is_ok(), "Expected Ok, got Err: {:?}", res.err()), + Err(e) => assert_eq!(res.unwrap_err().to_string(), e.to_string()), + } + } + } +} diff --git a/foundations/hulylake/server/src/mutex.rs b/foundations/hulylake/server/src/mutex.rs new file mode 100644 index 0000000000..261910dea5 --- /dev/null +++ b/foundations/hulylake/server/src/mutex.rs @@ -0,0 +1,23 @@ +use std::sync::Arc; + +use lockable::LockPool; +use uuid::Uuid; + +#[derive(Clone)] +pub struct KeyMutex { + lock_pool: Arc>, +} + +impl KeyMutex { + pub fn new() -> Self { + let lock_pool = Arc::new(LockPool::::new()); + + KeyMutex { lock_pool } + } + + pub async fn lock(&self, workspace: Uuid, key: String) -> impl Drop + '_ { + self.lock_pool + .async_lock(format!("{}:{}", workspace, key)) + .await + } +} diff --git a/foundations/hulylake/server/src/patch.rs b/foundations/hulylake/server/src/patch.rs new file mode 100644 index 0000000000..a431ff2e2f --- /dev/null +++ b/foundations/hulylake/server/src/patch.rs @@ -0,0 +1,654 @@ +use json_patch::PatchOperation as StandardPatchOperation; +use jsonptr::{Pointer, PointerBuf}; +use serde::{Deserialize, Serialize}; +use serde_json::{Number, Value, json}; +use thiserror::Error; +use tracing::*; +/// 'add' operation - increments a numeric value +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AddOperationExt { + /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location + /// within the target document where the operation is performed. + pub path: PointerBuf, + /// Value to add to the target location. + pub value: Value, + // When enabled, ensures that the operation does not overwrite existing fields + #[serde(default)] + pub safe: bool, +} + +/// 'inc' operation - increments a numeric value +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct IncOperationExt { + // JSON Pointer to the target location (must point to a numeric value) + pub path: PointerBuf, + // Amount to increment by (can be negative for decrement) + // Should be a number + pub value: Value, + // When enabled, ensures that the operation does not create new fields + #[serde(default)] + pub safe: bool, +} + +/// 'remove' operation - increments a numeric value +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct RemoveOperationExt { + /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location + /// within the target document where the operation is performed. + pub path: PointerBuf, + // When enabled, ensures that the operation does not create new fields + #[serde(default)] + pub safe: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "hop")] +#[serde(rename_all = "lowercase")] +pub enum HulyPatchOperation { + /// 'add' operation + Add(AddOperationExt), + /// 'inc' operation + Inc(IncOperationExt), + /// 'remove' operation + Remove(RemoveOperationExt), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PatchOperation { + Huly(HulyPatchOperation), + Standard(StandardPatchOperation), +} + +impl<'de> serde::Deserialize<'de> for PatchOperation { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = Value::deserialize(deserializer)?; + + let op = serde_json::from_value::(value.clone()); + if let Ok(op) = op { + Ok(Self::Standard(op)) + } else { + let standard_error = op.err().unwrap(); + serde_json::from_value::(value) + .map_err(|huly_error| { + serde::de::Error::custom(format!( + "Failed to deserialize as StandardPatchOperation: {}. Also failed to deserialize as HulyPatchOperation: {}", + standard_error, huly_error + )) + }) + .map(Self::Huly) + } + } +} + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum HulyPatchError { + #[error("invalid number")] + InvalidNumber, + #[error("patch error: {0}")] + PatchError(String), +} + +impl From for HulyPatchError { + fn from(err: json_patch::PatchError) -> Self { + HulyPatchError::PatchError(err.to_string()) + } +} + +pub fn apply(doc: &mut Value, patches: &[PatchOperation]) -> Result<(), HulyPatchError> { + for patch in patches { + if let Some(op) = match patch { + PatchOperation::Huly(huly_op) => match huly_op { + HulyPatchOperation::Add(op) => add(doc, &op.path, &op.value, op.safe), + HulyPatchOperation::Inc(op) => inc(doc, &op.path, &op.value, op.safe), + HulyPatchOperation::Remove(op) => remove(doc, &op.path, op.safe), + }, + PatchOperation::Standard(standard_op) => Ok(Some(standard_op.clone())), + }? { + if let Err(e) = json_patch::patch(doc, &[op.clone()]) { + error!("Failed to apply patch {:?}: {}", patch, e); + return Err(e.into()); + } + } + } + Ok(()) +} + +fn add( + doc: &Value, + path: &Pointer, + value: &Value, + safe: bool, +) -> Result, HulyPatchError> { + let target = doc.pointer(path.as_str()); + + Ok(if safe && target.is_some() { + None + } else { + Some(StandardPatchOperation::Add(json_patch::AddOperation { + path: path.to_owned(), + value: value.to_owned(), + })) + }) +} + +fn inc( + doc: &Value, + path: &Pointer, + value: &Value, + safe: bool, +) -> Result, HulyPatchError> { + let target = doc.pointer(path.as_str()); + + match (target, value.as_number()) { + (None, _) if safe => Ok(None), + + (None, Some(value)) => { + let op = StandardPatchOperation::Add(json_patch::AddOperation { + path: path.to_owned(), + value: serde_json::Value::Number(value.to_owned()), + }); + + Ok(Some(op)) + } + + (Some(_), None) => Err(HulyPatchError::InvalidNumber), + + (Some(serde_json::Value::Number(old_value)), Some(increment)) => { + let new_value = add_json_numbers(old_value, increment)?; + + let op = StandardPatchOperation::Replace(json_patch::ReplaceOperation { + path: path.to_owned(), + value: json!(new_value), + }); + + Ok(Some(op)) + } + + (Some(_), Some(_)) => Err(HulyPatchError::InvalidNumber), + (None, None) => Err(HulyPatchError::InvalidNumber), + } +} + +fn remove( + doc: &Value, + path: &Pointer, + safe: bool, +) -> Result, HulyPatchError> { + let target = doc.pointer(path.as_str()); + + Ok(if safe && target.is_none() { + None + } else { + Some(StandardPatchOperation::Remove( + json_patch::RemoveOperation { + path: path.to_owned(), + }, + )) + }) +} + +fn add_json_numbers(a: &Number, b: &Number) -> Result { + a.as_i64() + .and_then(|a| b.as_i64().map(|b| Number::from(a + b))) + .or_else(|| { + a.as_f64() + .and_then(|a| b.as_f64().map(|b| a + b)) + .and_then(Number::from_f64) + }) + .ok_or(HulyPatchError::InvalidNumber) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + fn test_add( + doc: Value, + path: PointerBuf, + value: Value, + safe: bool, + expected: Result, HulyPatchError>, + ) { + let mut doc = doc.clone(); + let res = add(&mut doc, &path, &value, safe); + + match expected { + Ok(op) => { + assert!(res.is_ok()); + assert_eq!(op, res.unwrap()); + } + Err(e) => { + assert!(res.is_err()); + assert_eq!(Some(e), res.err()); + } + } + } + + fn test_inc( + doc: Value, + path: PointerBuf, + value: Value, + safe: bool, + expected: Result, HulyPatchError>, + ) { + let mut doc = doc.clone(); + let res = inc(&mut doc, &path, &value, safe); + + match expected { + Ok(op) => { + assert!(res.is_ok()); + assert_eq!(op, res.unwrap()); + } + Err(e) => { + assert!(res.is_err()); + assert_eq!(Some(e), res.err()); + } + } + } + + fn test_remove( + doc: Value, + path: PointerBuf, + safe: bool, + expected: Result, HulyPatchError>, + ) { + let mut doc = doc.clone(); + let res = remove(&mut doc, &path, safe); + + match expected { + Ok(op) => { + assert!(res.is_ok()); + assert_eq!(op, res.unwrap()); + } + Err(e) => { + assert!(res.is_err()); + assert_eq!(Some(e), res.err()); + } + } + } + + #[test] + fn test_add_non_existing_object_field() { + test_add( + json!({}), + PointerBuf::from_tokens(["a"]), + json!(1), + false, + Ok(Some(StandardPatchOperation::Add( + json_patch::AddOperation { + path: PointerBuf::from_tokens(["a"]), + value: json!(1), + }, + ))), + ); + } + + #[test] + fn test_add_non_existing_object_field_safe() { + test_add( + json!({}), + PointerBuf::from_tokens(["a"]), + json!(1), + true, + Ok(Some(StandardPatchOperation::Add( + json_patch::AddOperation { + path: PointerBuf::from_tokens(["a"]), + value: json!(1), + }, + ))), + ); + } + + #[test] + fn test_add_existing_object_field() { + test_add( + json!({ "a": 1 }), + PointerBuf::from_tokens(["a"]), + json!(2), + false, + Ok(Some(StandardPatchOperation::Add( + json_patch::AddOperation { + path: PointerBuf::from_tokens(["a"]), + value: json!(2), + }, + ))), + ); + } + + #[test] + fn test_add_existing_object_field_safe() { + test_add( + json!({ "a": 1 }), + PointerBuf::from_tokens(["a"]), + json!(2), + true, + Ok(None), + ); + } + + #[test] + fn test_add_invalid_path() { + test_add( + json!({ "a": 1 }), + PointerBuf::from_tokens(["a", "b"]), + json!(2), + false, + Ok(Some(StandardPatchOperation::Add( + json_patch::AddOperation { + path: PointerBuf::from_tokens(["a", "b"]), + value: json!(2), + }, + ))), + ); + } + + #[test] + fn test_add_invalid_path_safe() { + test_add( + json!({ "a": 1 }), + PointerBuf::from_tokens(["a", "b"]), + json!(2), + true, + Ok(Some(StandardPatchOperation::Add( + json_patch::AddOperation { + path: PointerBuf::from_tokens(["a", "b"]), + value: json!(2), + }, + ))), + ); + } + + #[test] + fn test_inc_existing_object_field() { + test_inc( + json!({ "a": 1 }), + PointerBuf::from_tokens(["a"]), + json!(1), + false, + Ok(Some(StandardPatchOperation::Replace( + json_patch::ReplaceOperation { + path: PointerBuf::from_tokens(["a"]), + value: json!(2), + }, + ))), + ); + } + + #[test] + fn test_inc_non_existing_object_field() { + test_inc( + json!({}), + PointerBuf::from_tokens(["a"]), + json!(1), + false, + Ok(Some(StandardPatchOperation::Add( + json_patch::AddOperation { + path: PointerBuf::from_tokens(["a"]), + value: json!(1), + }, + ))), + ); + } + + #[test] + fn test_inc_non_existing_object_field_safe() { + test_inc( + json!({}), + PointerBuf::from_tokens(["a"]), + json!(1), + true, + Ok(None), + ); + } + + #[test] + fn test_inc_existing_array_item() { + test_inc( + json!({ "a": [0, 1, 2] }), + PointerBuf::from_tokens(["a", "0"]), + json!(1), + false, + Ok(Some(StandardPatchOperation::Replace( + json_patch::ReplaceOperation { + path: PointerBuf::from_tokens(["a", "0"]), + value: json!(1), + }, + ))), + ); + } + + #[test] + fn test_inc_non_existing_array_item() { + test_inc( + json!({ "a": [0, 1, 2] }), + PointerBuf::from_tokens(["a", "3"]), + json!(3), + false, + Ok(Some(StandardPatchOperation::Add( + json_patch::AddOperation { + path: PointerBuf::from_tokens(["a", "3"]), + value: json!(3), + }, + ))), + ); + } + + #[test] + fn test_inc_non_existing_array_item_safe() { + test_inc( + json!({ "a": [0, 1, 2] }), + PointerBuf::from_tokens(["a", "3"]), + json!(3), + true, + Ok(None), + ); + } + + #[test] + fn test_inc_non_number_field() { + test_inc( + json!({ "a": "b" }), + PointerBuf::from_tokens(["a"]), + json!(1), + false, + Err(HulyPatchError::InvalidNumber), + ); + } + + #[test] + fn test_inc_by_non_number() { + test_inc( + json!({ "a": "1" }), + PointerBuf::from_tokens(["a"]), + json!("one"), + false, + Err(HulyPatchError::InvalidNumber), + ); + } + + #[test] + fn test_remove_existing_object_field() { + test_remove( + json!({ "a": 1 }), + PointerBuf::from_tokens(["a"]), + false, + Ok(Some(StandardPatchOperation::Remove( + json_patch::RemoveOperation { + path: PointerBuf::from_tokens(["a"]), + }, + ))), + ); + } + + #[test] + fn test_remove_non_existing_object_field() { + test_remove( + json!({}), + PointerBuf::from_tokens(["a"]), + false, + Ok(Some(StandardPatchOperation::Remove( + json_patch::RemoveOperation { + path: PointerBuf::from_tokens(["a"]), + }, + ))), + ); + } + + #[test] + fn test_remove_non_existing_object_field_safe() { + test_remove(json!({}), PointerBuf::from_tokens(["a"]), true, Ok(None)); + } + + #[test] + fn test_add_json_numbers_i_i() { + let res = add_json_numbers(&Number::from(1), &Number::from(1)); + assert_eq!(res, Ok(Number::from(2))); + } + + #[test] + fn test_add_json_numbers_i_f() { + let res = add_json_numbers(&Number::from(1), &Number::from_f64(1.0).unwrap()); + assert_eq!(res, Ok(Number::from_f64(2.0).unwrap())); + } + + #[test] + fn test_add_json_numbers_f_i() { + let res = add_json_numbers(&Number::from_f64(1.0).unwrap(), &Number::from(1)); + assert_eq!(res, Ok(Number::from_f64(2.0).unwrap())); + } + + #[test] + fn test_add_json_numbers_f_f() { + let res = add_json_numbers( + &Number::from_f64(1.0).unwrap(), + &Number::from_f64(1.0).unwrap(), + ); + assert_eq!(res, Ok(Number::from_f64(2.0).unwrap())); + } + + #[test] + fn test_patch() { + let mut doc = json!({}); + + let patches = vec![ + PatchOperation::Huly(HulyPatchOperation::Add(AddOperationExt { + path: PointerBuf::from_tokens(["a"]), + value: json!([]), + safe: false, + })), + PatchOperation::Huly(HulyPatchOperation::Inc(IncOperationExt { + path: PointerBuf::from_tokens(["a", "0"]), + value: json!(1), + safe: true, + })), + PatchOperation::Huly(HulyPatchOperation::Add(AddOperationExt { + path: PointerBuf::from_tokens(["a", "0"]), + value: json!(0), + safe: false, + })), + PatchOperation::Huly(HulyPatchOperation::Inc(IncOperationExt { + path: PointerBuf::from_tokens(["a", "0"]), + value: json!(2), + safe: true, + })), + ]; + + let res = apply(&mut doc, &patches); + assert!(res.is_ok()); + assert_eq!( + doc, + json!({ + "a": [2] + }) + ); + } + + #[test] + fn test_patch_err() { + let mut doc = json!({}); + + let patches = vec![ + PatchOperation::Standard(StandardPatchOperation::Add(json_patch::AddOperation { + path: PointerBuf::from_tokens(["a"]), + value: json!([]), + })), + PatchOperation::Standard(StandardPatchOperation::Add(json_patch::AddOperation { + path: PointerBuf::from_tokens(["a", "b"]), + value: json!(1), + })), + ]; + + let res = apply(&mut doc, &patches); + assert_eq!( + res, + Err(HulyPatchError::PatchError(String::from( + "operation '/0' failed at path '/a/b': path is invalid" + ))) + ); + } + + #[test] + fn test_deserialize_add_safe() { + let patch = r#"{ "hop": "add", "path": "/a", "value": 1, "safe": true }"#; + let res = serde_json::from_str::(patch); + assert!(res.is_ok()); + assert_eq!( + res.unwrap(), + PatchOperation::Huly(HulyPatchOperation::Add(AddOperationExt { + path: PointerBuf::from_tokens(["a"]), + value: json!(1), + safe: true, + })) + ); + } + + #[test] + fn test_deserialize_add_unsafe() { + let patch = r#"{ "hop": "add", "path": "/a", "value": 1 }"#; + let res = serde_json::from_str::(patch); + assert!(res.is_ok()); + assert_eq!( + res.unwrap(), + PatchOperation::Huly(HulyPatchOperation::Add(AddOperationExt { + path: PointerBuf::from_tokens(["a"]), + value: json!(1), + safe: false, + })) + ); + } + + #[test] + fn test_deserialize_inc_safe() { + let patch = r#"{ "hop": "inc", "path": "/a", "value": 1, "safe": true }"#; + let res = serde_json::from_str::(patch); + assert!(res.is_ok()); + assert_eq!( + res.unwrap(), + PatchOperation::Huly(HulyPatchOperation::Inc(IncOperationExt { + path: PointerBuf::from_tokens(["a"]), + value: json!(1), + safe: true, + })) + ); + } + + #[test] + fn test_deserialize_inc_unsafe() { + let patch = r#"{ "hop": "inc", "path": "/a", "value": 1 }"#; + let res = serde_json::from_str::(patch); + assert!(res.is_ok()); + assert_eq!( + res.unwrap(), + PatchOperation::Huly(HulyPatchOperation::Inc(IncOperationExt { + path: PointerBuf::from_tokens(["a"]), + value: json!(1), + safe: false, + })) + ); + } +} diff --git a/foundations/hulylake/server/src/postgres.rs b/foundations/hulylake/server/src/postgres.rs new file mode 100644 index 0000000000..bd52e0b769 --- /dev/null +++ b/foundations/hulylake/server/src/postgres.rs @@ -0,0 +1,220 @@ +use std::pin::Pin; + +use bb8_postgres::PostgresConnectionManager; +use bytes::Bytes; +use serde::de::DeserializeOwned; +use tokio_postgres::NoTls; +use tokio_postgres::{self as pg}; +use tracing::*; + +use crate::config::CONFIG; + +pub type Pool = bb8::Pool>; + +#[derive(thiserror::Error, Debug)] +pub enum DbError { + #[error(transparent)] + Pool(#[from] bb8::RunError), + + #[error(transparent)] + Db(#[from] tokio_postgres::Error), + + #[error(transparent)] + Json(#[from] serde_json::Error), + + #[error(transparent)] + Refinery(#[from] refinery::Error), + + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +pub async fn pool() -> anyhow::Result { + let manager = bb8_postgres::PostgresConnectionManager::new_from_stringlike( + &CONFIG.db_connection, + tokio_postgres::NoTls, + )?; + + #[derive(Debug)] + struct ConnectionCustomizer; + + impl bb8::CustomizeConnection for ConnectionCustomizer { + fn on_acquire<'a>( + &'a self, + client: &'a mut pg::Client, + ) -> Pin> + Send + 'a>> { + Box::pin(async { + client + .execute("set search_path to $1", &[&CONFIG.db_scheme]) + .await + .unwrap(); + Ok(()) + }) + } + } + + let pool = bb8::Pool::builder() + .max_size(15) + .connection_customizer(Box::new(ConnectionCustomizer)) + .build(manager) + .await?; + + { + let mut connection = pool.dedicated_connection().await?; + + // query params cannot be bound in ddl statements + connection + .execute( + &format!("create schema if not exists {}", CONFIG.db_scheme), + &[], + ) + .await?; + + refinery::embed_migrations!("etc/migrations"); + + let report = migrations::runner() + .set_migration_table_name("migrations") + .run_async(&mut connection) + .await?; + + for m in report.applied_migrations().iter() { + info!(migration = m.to_string(), "apply migration"); + } + } + + Ok(pool) +} + +#[instrument(level = "debug", skip_all)] +async fn get_connection( + pool: &Pool, +) -> Result>, DbError> { + Ok(pool.get().await?) +} + +#[instrument(level = "debug", skip_all)] +pub async fn find_blob_by_hash(pool: &Pool, hash: &str) -> anyhow::Result, DbError> { + let connection = get_connection(pool).await?; + + let blob = connection + .query("select key from blob where hash = $1", &[&hash]) + .await?; + + Ok(match blob.as_slice() { + [found] => Some(found.get::<_, String>("key")), + [] => None, + + _ => panic!(), + }) +} + +#[instrument(level = "debug", skip_all)] +pub async fn insert_blob(pool: &Pool, key: &str, hash: &str) -> anyhow::Result<(), DbError> { + let connection = get_connection(pool).await?; + + connection + .execute( + "insert into blob (key, hash) values ($1, $2)", + &[&key, &hash], + ) + .await?; + + Ok(()) +} + +#[derive(Debug, Clone)] +pub struct ObjectPart { + pub inline: Option>, + pub data: T, +} + +#[instrument(level = "debug", skip_all)] +pub async fn find_parts( + pool: &Pool, + workspace: uuid::Uuid, + key: &str, +) -> anyhow::Result>, DbError> { + let connection = get_connection(pool).await?; + + let rows = connection + .query( + "select part, data, inline from object where workspace = $1 and key = $2 order by part", + &[&workspace, &key], + ) + .await?; + + let mut parts = Vec::with_capacity(rows.len()); + + for row in rows { + let data = row.get::<_, serde_json::Value>("data"); + let inline = row.get::<_, Option>>("inline"); + + let data = serde_json::from_value(data)?; + parts.push(ObjectPart { inline, data }) + } + + Ok(parts) +} + +#[instrument(level = "debug", skip_all)] +pub async fn append_part( + pool: &Pool, + workspace: uuid::Uuid, + key: &str, + part: u32, + inline: Option, + data: &D, +) -> anyhow::Result<(), DbError> { + let connection = get_connection(pool).await?; + + let data = serde_json::to_value(data)?; + let inline = inline.map(|b| b.to_vec()); + + connection + .execute( + "insert into object (workspace, key, part, inline, data) values ($1, $2, $3, $4, $5)", + &[&workspace, &key, &(part as i32), &inline, &data], + ) + .await?; + + Ok(()) +} + +#[instrument(level = "debug", skip_all)] +pub async fn set_part( + pool: &Pool, + workspace: uuid::Uuid, + key: &str, + inline: Option, + data: &D, +) -> anyhow::Result<(), DbError> { + let mut connection = get_connection(pool).await?; + + let transaction = connection.transaction().await?; + + transaction + .execute( + "delete from object where workspace = $1 and key = $2", + &[&workspace, &key], + ) + .await?; + + let data = serde_json::to_value(data)?; + let inline = inline.map(|b| b.to_vec()); + + transaction + .execute( + r#" + insert into object (workspace, key, part, inline, data) values ($1, $2, 0, $3, $4) + on conflict (workspace, key, part) do update set + inline = $3, + data = $4 + "#, + &[&workspace, &key, &inline, &data], + ) + .await?; + + transaction.commit().await?; + + Ok(()) +} diff --git a/foundations/hulylake/server/src/recovery.rs b/foundations/hulylake/server/src/recovery.rs new file mode 100644 index 0000000000..cf3a399157 --- /dev/null +++ b/foundations/hulylake/server/src/recovery.rs @@ -0,0 +1,91 @@ +use aws_sdk_s3::error::SdkError; +use aws_sdk_s3::operation::put_object::PutObjectError; +use bytes::Bytes; + +use crate::conditional::ConditionalMatch; +use crate::config::CONFIG; +use crate::{handlers::PartData, s3::S3Client}; + +#[derive(thiserror::Error, Debug)] +pub enum RecoveryError { + #[error("S3 Error: {0}")] + S3(String), + + #[error("Precondition Failed")] + PreconditionFailed, + + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +impl From for RecoveryError { + fn from(err: serde_json::Error) -> Self { + RecoveryError::Other(err.into()) + } +} + +impl From> for RecoveryError { + fn from(err: SdkError) -> Self { + match err { + SdkError::ServiceError(service_err) if service_err.raw().status().as_u16() == 412 => { + RecoveryError::PreconditionFailed + } + _ => RecoveryError::S3(err.to_string()), + } + } +} + +pub fn object_etag(parts: Vec<&PartData>) -> anyhow::Result { + let body = Bytes::from(serde_json::to_string(&parts)?); + let digest = md5::compute(body); + Ok(format!("{:x}", digest)) +} + +#[tracing::instrument(level = "debug", skip_all)] +pub async fn set_object( + s3: &S3Client, + workspace: uuid::Uuid, + key: &str, + parts: Vec<&PartData>, + conditions: Option, +) -> Result<(), RecoveryError> { + let s3_bucket = &CONFIG.s3_bucket; + + let key = format!("blob/{}/{}", workspace, key); + let body = Bytes::from(serde_json::to_string(&parts)?); + + let mut cmd = s3 + .put_object() + .bucket(s3_bucket) + .key(key) + .body(body.into()) + .content_type("application/json"); + + cmd = match conditions { + Some(ConditionalMatch::IfMatch(etag)) => cmd.if_match(etag), + Some(ConditionalMatch::IfNoneMatch(etag)) => cmd.if_none_match(etag), + None => cmd, + }; + + cmd.send().await?; + + Ok(()) +} + +#[tracing::instrument(level = "debug", skip_all)] +pub async fn set_blob(s3: &S3Client, key: &str, hash: &str) -> Result<(), RecoveryError> { + let s3_bucket = &CONFIG.s3_bucket; + + let key = format!("hash/{}", key); + let body = Bytes::from(hash.to_string()); + + s3.put_object() + .bucket(s3_bucket) + .key(key) + .body(body.into()) + .content_type("text/plain") + .send() + .await?; + + Ok(()) +} diff --git a/foundations/hulylake/server/src/s3.rs b/foundations/hulylake/server/src/s3.rs new file mode 100644 index 0000000000..8d19a01120 --- /dev/null +++ b/foundations/hulylake/server/src/s3.rs @@ -0,0 +1,177 @@ +use std::error::Error as StdError; + +use anyhow::Result; +use aws_config::BehaviorVersion; +use aws_sdk_s3::{ + Config, + types::{CompletedMultipartUpload, CompletedPart}, +}; +use blake3::{Hash, Hasher}; +use bytes::{Bytes, BytesMut}; +use futures::stream::StreamExt; +use futures_util::Stream; +use tracing::*; + +pub type S3Client = aws_sdk_s3::Client; + +pub async fn client() -> S3Client { + let ref sdk_config = aws_config::defaults(BehaviorVersion::latest()) + .load() + .await + .into_builder() + .build(); + + let s3_config = Config::from(sdk_config) + .to_builder() + .force_path_style(true) + .build(); + + S3Client::from_conf(s3_config) +} + +pub struct Upload { + pub hash: Hash, + pub length: usize, + pub parts_count: usize, +} + +async fn multipart_upload_stream( + s3: &S3Client, + bucket: &str, + key: &str, + upload_id: &str, + mut source: S, +) -> Result<(CompletedMultipartUpload, Upload)> +where + S: Stream> + Unpin, + E: StdError + Send + Sync + 'static, +{ + debug!("upload start"); + + let upload_part = async |number, buffer: Bytes| -> Result { + let upload = s3 + .upload_part() + .bucket(bucket) + .key(key) + .upload_id(upload_id) + .body(buffer.into()) + .part_number(number) + .send() + .await?; + + let part = CompletedPart::builder() + .e_tag(upload.e_tag.unwrap()) + .part_number(number) + .build(); + + Ok(part) + }; + + let mut buffer = BytesMut::with_capacity(1024 * 1024 * 6); + let mut complete = CompletedMultipartUpload::builder(); + let mut part_number = 1; + let mut hash = Hasher::new(); + let mut total_in = 0; + let mut length = 0; + + while let Some(part) = source.next().await { + let part = part.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + + hash.update(&part); + + total_in += part.len(); + + buffer.extend_from_slice(&part); + + // each part must be at least 5MB + if buffer.len() > 1024 * 1024 * 5 { + trace!(length = buffer.len(), part_number, "upload part"); + + length += buffer.len(); + + let uploaded = upload_part(part_number, buffer.freeze()).await?; + + complete = complete.parts(uploaded); + + buffer = BytesMut::new(); + part_number += 1; + } + } + + // the last part + if buffer.len() > 0 { + length += buffer.len(); + + trace!(length = buffer.len(), part_number, "upload part"); + let uploaded = upload_part(part_number, buffer.freeze()).await?; + complete = complete.parts(uploaded); + } + + assert_eq!(total_in, length); + + let hash = hash.finalize(); + + let complete = complete.build(); + let parts_count = complete.parts().len(); + + Ok(( + complete, + Upload { + hash, + length, + parts_count, + }, + )) +} + +#[tracing::instrument(level = "debug", skip_all)] +pub async fn multipart_upload( + s3: &S3Client, + bucket: &str, + key: &str, + source: S, +) -> Result +where + S: Stream> + Unpin, + E: StdError + Send + Sync + 'static, +{ + let span = Span::current(); + + let create_multipart = s3 + .create_multipart_upload() + .bucket(bucket) + .key(key) + .send() + .await?; + + let upload_id = create_multipart.upload_id().unwrap(); + + span.record("upload", &upload_id[upload_id.len().saturating_sub(16)..]); + + match multipart_upload_stream(s3, bucket, key, upload_id, source).await { + Ok((complete, upload)) => { + s3.complete_multipart_upload() + .bucket(bucket) + .key(key) + .multipart_upload(complete) + .upload_id(upload_id) + .send() + .await?; + + debug!(hash = %upload.hash, length = upload.length, "upload complete"); + + Ok(upload) + } + Err(error) => { + s3.abort_multipart_upload() + .bucket(bucket) + .key(key) + .upload_id(upload_id) + .send() + .await?; + + error!(%error, "upload error"); + Err(error) + } + } +} diff --git a/foundations/hulylake/tests/Cargo.toml b/foundations/hulylake/tests/Cargo.toml new file mode 100644 index 0000000000..0c4bd1c291 --- /dev/null +++ b/foundations/hulylake/tests/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tests" +version = "0.1.0" +edition = "2024" + +[dependencies] +tanu = "0.9.0" +tokio = { version = "1.47.1", features = ["full"] } +uuid = { version = "1.18.0" } +hulyrs = { git = "https://github.com/hcengineering/hulyrs.git", features = [ + "actix", +] } +secrecy = "0.10.3" +reqwest = { version = "0.12.23", default-features = false, features = [ + "json", + "rustls-tls", + "stream", +] } +rand = "0.9.2" +hex = "0.4.3" +serde_json = "1.0.143" +futures = "0.3.31" diff --git a/foundations/hulylake/tests/src/auth.rs b/foundations/hulylake/tests/src/auth.rs new file mode 100644 index 0000000000..17fe163cf1 --- /dev/null +++ b/foundations/hulylake/tests/src/auth.rs @@ -0,0 +1,41 @@ +use secrecy::ExposeSecret; +use tanu::{ + check, check_eq, check_ne, eyre, + http::{Client, Method, StatusCode}, +}; + +use crate::config::CONFIG; +use crate::util::*; + +// #[tanu::test("HEAD", Method::HEAD)] +#[tanu::test("GET", Method::GET)] +#[tanu::test("PUT", Method::PUT)] +#[tanu::test("POST", Method::POST)] +#[tanu::test("DELETE", Method::DELETE)] +async fn auth_without_token_unauthorized(_: &str, method: Method) -> eyre::Result<()> { + let http = Client::new(); + + let res = http.request(&method, &path(&random_key())).send().await?; + check!(!res.status().is_success(), "method: {method}"); + check_eq!(StatusCode::UNAUTHORIZED, res.status(), "method: {method}"); + + Ok(()) +} + +// #[tanu::test("HEAD", Method::HEAD)] +#[tanu::test("GET", Method::GET)] +#[tanu::test("PUT", Method::PUT)] +#[tanu::test("POST", Method::POST)] +// #[tanu::test("DELETE", Method::DELETE)] +async fn auth_with_token(_: &str, method: Method) -> eyre::Result<()> { + let http = Client::new(); + + let res = http + .request(&method, &path(&random_key())) + .bearer_auth(CONFIG.token_valid.expose_secret()) + .send() + .await?; + check_ne!(StatusCode::UNAUTHORIZED, res.status(), "method: {method}"); + + Ok(()) +} diff --git a/foundations/hulylake/tests/src/compact.rs b/foundations/hulylake/tests/src/compact.rs new file mode 100644 index 0000000000..f482d14c93 --- /dev/null +++ b/foundations/hulylake/tests/src/compact.rs @@ -0,0 +1,202 @@ +use serde_json::{self as json, Value, json}; +use tanu::{check, eyre, http::Client}; + +use crate::util::*; + +#[tanu::test(50)] +#[tanu::test(100)] +#[tanu::test(200)] +#[tanu::test(500)] +pub async fn compact_json(count: usize) -> eyre::Result<()> { + let key = random_key(); + + let http = Client::new(); + + let initial = json!({ + "a": 0 + }); + + // create new blob + let res = http + .key_put(&key) + .body(json::to_string(&initial)?) + .header("huly-merge-strategy", "jsonpatch") + .header("content-type", "application/json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + for i in 0..count { + let patch = json!([ + { "op": "replace", "path": "/a", "value": i + 1}, + ]); + + let body: String = json::to_string(&patch)?; + let res = http + .key_patch(&key) + .body(body) + .header("content-type", "application/json-patch+json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + } + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + let json = res.json::().await?; + assert_eq!(json, json!({ "a": count })); + + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + let json = res.json::().await?; + assert_eq!(json, json!({ "a": count })); + + Ok(()) +} + +#[tanu::test(50, 10)] +#[tanu::test(100, 10)] +#[tanu::test(100, 50)] +#[tanu::test(100, 100)] +pub async fn compact_text(count: usize, size: usize) -> eyre::Result<()> { + let key = random_key(); + let patch = random_text(1024 * size); + + let http = Client::new(); + + let initial = json!({ + "a": 0 + }); + + // create new blob + let res = http + .key_put(&key) + .body(json::to_string(&initial)?) + .header("content-type", "text/plain") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + for _ in 0..count { + let body: String = json::to_string(&patch)?; + let res = http + .key_patch(&key) + .body(body) + .header("content-type", "text/plain") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + } + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + + Ok(()) +} + +#[tanu::test(50, 10)] +#[tanu::test(100, 10)] +#[tanu::test(100, 50)] +#[tanu::test(100, 100)] +pub async fn compact_get_text(count: usize, size: usize) -> eyre::Result<()> { + let key = random_key(); + let patch = random_text(1024 * size); + + let http = Client::new(); + + let initial = json!({ + "a": 0 + }); + + // create new blob + let res = http + .key_put(&key) + .body(json::to_string(&initial)?) + .header("content-type", "text/plain") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + for _ in 0..count { + let body: String = json::to_string(&patch)?; + let res = http + .key_patch(&key) + .body(body) + .header("content-type", "text/plain") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + } + + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + + Ok(()) +} + +#[tanu::test(50)] +#[tanu::test(100)] +pub async fn compact_get_json(count: usize) -> eyre::Result<()> { + let key = random_key(); + let text = random_text(1024 * 10); + let http = Client::new(); + + let initial = json!({ + "a": [] + }); + + // create new blob + let res = http + .key_put(&key) + .body(json::to_string(&initial)?) + .header("huly-merge-strategy", "jsonpatch") + .header("content-type", "application/json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + for i in 0..count { + let patch = json!([ + { "op": "add", "path": format!("/a/{}", i), "value": text}, + ]); + + let body: String = json::to_string(&patch)?; + let res = http + .key_patch(&key) + .body(body) + .header("content-type", "application/json-patch+json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + } + + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + + Ok(()) +} diff --git a/foundations/hulylake/tests/src/config.rs b/foundations/hulylake/tests/src/config.rs new file mode 100644 index 0000000000..fb9305dd12 --- /dev/null +++ b/foundations/hulylake/tests/src/config.rs @@ -0,0 +1,44 @@ +use std::sync::LazyLock; + +use hulyrs::services::jwt::ClaimsBuilder; +use secrecy::SecretString; +use uuid::{Uuid, uuid}; + +pub struct Config { + pub base_url: String, + pub workspace: Uuid, + pub token_valid: SecretString, + pub token_invalid: SecretString, +} + +pub static CONFIG: LazyLock = LazyLock::new(|| { + let config = tanu::get_config(); + + let secret = SecretString::from("secret"); + + let workspace = uuid!("dc06ad17-58af-4166-beca-48e40c990d51"); + let workspace_invalid = uuid!("2f52775a-57a1-4fc1-aa90-d6cde40d4b5a"); + let account = uuid!("34cee70d-52c4-47a1-a33a-7fec6e3e7ada"); + + let claims_valid = ClaimsBuilder::default() + .workspace(workspace) + .account(account) + .build() + .unwrap(); + + let claims_invalid = ClaimsBuilder::default() + .workspace(workspace_invalid) + .account(account) + .build() + .unwrap(); + + let token_valid = claims_valid.encode(&secret).unwrap(); + let token_invalid = claims_invalid.encode(&secret).unwrap(); + + Config { + base_url: config.get_str("base_url").unwrap().to_string(), + workspace, + token_valid, + token_invalid, + } +}); diff --git a/foundations/hulylake/tests/src/get.rs b/foundations/hulylake/tests/src/get.rs new file mode 100644 index 0000000000..dde207b920 --- /dev/null +++ b/foundations/hulylake/tests/src/get.rs @@ -0,0 +1,156 @@ +use tanu::{ + check, check_eq, eyre, + http::{self, Client}, +}; + +use crate::util::*; + +#[tanu::test] +pub async fn get_unknown() -> eyre::Result<()> { + let http = Client::new(); + + let res = http.key_get(&random_key()).send().await?; + check!(!res.status().is_success()); + check_eq!(http::StatusCode::NOT_FOUND, res.status()); + + Ok(()) +} + +#[tanu::test] +pub async fn get_known() -> eyre::Result<()> { + let key = random_key(); + let text = random_text(1024); + + let http = Client::new(); + + let res = http.key_put(&key).body(text.clone()).send().await?; + check!(res.status().is_success()); + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + check_eq!( + Some(text.len().to_string().as_str()), + res.header("content-length"), + ); + check!(res.header("etag").is_some()); + check_eq!(text, res.text().await?); + + Ok(()) +} + +#[tanu::test] +pub async fn get_conditional() -> eyre::Result<()> { + let key = random_key(); + let text = random_text(1024); + + let http = Client::new(); + + let res = http.key_put(&key).body(text.clone()).send().await?; + check!(res.status().is_success()); + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + let etag = res.header("etag").expect("ETag not found"); + + // Test without If-None-Match (normal GET) + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + check_eq!(text, res.text().await?); + + // Test with If-None-Match: * + let res = http + .key_get(&key) + .header("If-None-Match", "*") + .send() + .await?; + check_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + + // Test with correct ETag + let res = http + .key_get(&key) + .header("If-None-Match", etag) + .send() + .await?; + check_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + + // Test with incorrect ETag + let res = http + .key_get(&key) + .header("If-None-Match", "\"invalid-etag\"") + .send() + .await?; + check!(res.status().is_success()); + check_eq!(text, res.text().await?); + + Ok(()) +} + +#[tanu::test] +pub async fn get_partial() -> eyre::Result<()> { + let key = random_key(); + let text = random_text(1024 * 1024 * 5); + + let http = Client::new(); + + let res = http.key_put(&key).body(text.clone()).send().await?; + check!(res.status().is_success()); + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + check_eq!(res.header("accept-ranges"), Some("bytes")); + + let res = http + .key_get(&key) + .header("range", "bytes=0-127") + .send() + .await?; + check_eq!(res.status(), http::StatusCode::PARTIAL_CONTENT); + check_eq!(res.header("content-length"), Some("128")); + check_eq!(res.header("content-range"), Some("bytes 0-127/5242880")); + + let res = http + .key_get(&key) + .header("range", "bytes=0-5242879") + .send() + .await?; + check_eq!(res.status(), http::StatusCode::OK); + check_eq!(res.header("content-length"), Some("5242880")); + check_eq!(res.header("content-range"), Some("bytes 0-5242879/5242880")); + + Ok(()) +} + +#[tanu::test] +pub async fn get_partial_inline() -> eyre::Result<()> { + let key = random_key(); + let text = random_text(1024); + + let http = Client::new(); + + let res = http.key_put(&key).body(text.clone()).send().await?; + check!(res.status().is_success()); + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + check_eq!(res.header("accept-ranges"), Some("bytes")); + + let res = http + .key_get(&key) + .header("range", "bytes=0-31") + .send() + .await?; + check_eq!(res.status(), http::StatusCode::PARTIAL_CONTENT); + check_eq!(res.header("content-length"), Some("32")); + check_eq!(res.header("content-range"), Some("bytes 0-31/1024")); + + let res = http + .key_get(&key) + .header("range", "bytes=0-1023") + .send() + .await?; + check_eq!(res.status(), http::StatusCode::OK); + check_eq!(res.header("content-length"), Some("1024")); + check_eq!(res.header("content-range"), Some("bytes 0-1023/1024")); + + Ok(()) +} diff --git a/foundations/hulylake/tests/src/head.rs b/foundations/hulylake/tests/src/head.rs new file mode 100644 index 0000000000..1cdd808e84 --- /dev/null +++ b/foundations/hulylake/tests/src/head.rs @@ -0,0 +1,111 @@ +use serde_json::{self as json, json}; +use tanu::{ + check, check_eq, eyre, + http::{self, Client}, +}; + +use crate::util::*; + +#[tanu::test] +pub async fn head_unknown() -> eyre::Result<()> { + let http = Client::new(); + + let res = http.key_head(&random_key()).send().await?; + check!(!res.status().is_success()); + check_eq!(http::StatusCode::NOT_FOUND, res.status()); + + Ok(()) +} + +#[tanu::test] +pub async fn head_known() -> eyre::Result<()> { + let key = random_key(); + let text = random_text(1024); + + let http = Client::new(); + + let res = http.key_put(&key).body(text.clone()).send().await?; + check!(res.status().is_success()); + + let res = http.key_head(&key).send().await?; + check!(res.status().is_success()); + check_eq!( + Some(text.len().to_string().as_str()), + res.header("content-length"), + ); + check!(res.header("etag").is_some()); + check_eq!("", res.text().await?); + + Ok(()) +} + +#[tanu::test] +pub async fn head_known_with_jsonpatch() -> eyre::Result<()> { + let key = random_key(); + let payload = json!({ "foo": "bar" }); + + let http = Client::new(); + + // create new blob + let res = http + .key_put(&key) + .body(json::to_string(&payload)?) + .header("huly-merge-strategy", "jsonpatch") + .header("content-type", "application/json") + .send() + .await?; + check!(res.status().is_success()); + + let res = http.key_head(&key).send().await?; + check!(res.status().is_success()); + // despite the fact we are not setting content-length + // actix forces it to be returned as 0 + check_eq!(Some("0"), res.header("content-length")); + + Ok(()) +} + +#[tanu::test] +pub async fn head_conditional() -> eyre::Result<()> { + let key = random_key(); + let text = random_text(1024); + + let http = Client::new(); + + let res = http.key_put(&key).body(text.clone()).send().await?; + check!(res.status().is_success()); + + let res = http.key_head(&key).send().await?; + check!(res.status().is_success()); + let etag = res.header("etag").expect("ETag not found"); + + // Test without If-None-Match (normal GET) + let res = http.key_head(&key).send().await?; + check!(res.status().is_success()); + + // Test with If-None-Match: * + let res = http + .key_head(&key) + .header("If-None-Match", "*") + .send() + .await?; + check_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + + // Test with correct ETag + let res = http + .key_head(&key) + .header("If-None-Match", etag) + .send() + .await?; + check_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + + // Test with incorrect ETag + let res = http + .key_head(&key) + .header("If-None-Match", "\"invalid-etag\"") + .send() + .await?; + check!(res.status().is_success()); + + Ok(()) +} diff --git a/foundations/hulylake/tests/src/main.rs b/foundations/hulylake/tests/src/main.rs new file mode 100644 index 0000000000..c6fe12ba78 --- /dev/null +++ b/foundations/hulylake/tests/src/main.rs @@ -0,0 +1,20 @@ +mod auth; +mod compact; +mod config; +mod get; +mod head; +mod patch; +mod put; +mod sanity; +mod util; + +use tanu::eyre; + +#[tanu::main] +#[tokio::main] +async fn main() -> eyre::Result<()> { + let runner = run(); + let app = tanu::App::new(); + app.run(runner).await?; + Ok(()) +} diff --git a/foundations/hulylake/tests/src/patch.rs b/foundations/hulylake/tests/src/patch.rs new file mode 100644 index 0000000000..9d4f5c1c8d --- /dev/null +++ b/foundations/hulylake/tests/src/patch.rs @@ -0,0 +1,393 @@ +use futures::future::join_all; +use hulyrs::StatusCode; +use serde_json::{self as json, Value, json}; +use tanu::{check, check_eq, check_ne, eyre, http::Client}; + +use crate::util::*; + +#[tanu::test((10, 10))] +pub async fn put_and_patch_content((initial, patch): (usize, usize)) -> eyre::Result<()> { + let key = random_key(); + let initial = random_text(1024 * initial); + let patch = random_text(1024 * patch); + + let http = Client::new(); + + // patch non-existing blob + let res = http.key_patch(&key).body(patch.clone()).send().await?; + check_eq!(res.status(), StatusCode::NOT_FOUND, "{:#?}", res); + + // create new blob + let res = http.key_put(&key).body(initial.clone()).send().await?; + check!(res.status().is_success(), "{:#?}", res); + + // check content + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + check_eq!(initial, res.text().await?); + + // patch + let res = http.key_patch(&key).body(patch.clone()).send().await?; + check!(res.status().is_success(), "{:#?}", res); + + // check content + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + check_eq!(res.text().await?, format!("{}{}", initial, patch)); + + Ok(()) +} + +use super::put::Body; + +#[tanu::test(1, None, Body::Text("{}"), StatusCode::BAD_REQUEST)] +#[tanu::test(2, Some("application/json"), Body::Text("{}"), StatusCode::BAD_REQUEST)] +#[tanu::test( + 3, + Some("application/json-patch+json"), + Body::Text("{"), + StatusCode::BAD_REQUEST +)] +#[tanu::test( + 4, + Some("application/json-patch+json"), + Body::Text("{}"), + StatusCode::BAD_REQUEST +)] +pub async fn put_and_patch_json_patch( + _: usize, + content_type: Option<&str>, + body: Body, + status: StatusCode, +) -> eyre::Result<()> { + let key = random_key(); + + let http = Client::new(); + + // create new blob + let res = http + .key_put(&key) + .body("{}") + .header("huly-merge-strategy", "jsonpatch") + .header("content-type", "application/json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + let mut req = http.key_patch(&key); + + match body { + Body::Random(size) => { + req = req.body(random_body(size)); + } + Body::Text(text) => { + req = req.body(text.to_string()); + } + } + + if let Some(content_type) = content_type { + req = req.header("content-type", content_type); + } + + let res = req.send().await?; + + check!(res.status() == status); + + Ok(()) +} + +#[tanu::test] +async fn get_json_patch() -> eyre::Result<()> { + let key = random_key(); + + let http = Client::new(); + + let initial = json!({ + "a": 1, + "b": 2, + "c": 3 + }); + + // create new blob + let res = http + .key_put(&key) + .body(json::to_string(&initial)?) + .header("huly-merge-strategy", "jsonpatch") + .header("content-type", "application/json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + let patch = json!([ + { "op": "add", "path": "/a", "value": 4 }, + { "op": "replace", "path": "/b", "value": 5 }, + { "op": "remove", "path": "/c" } + ]); + + let res = http + .key_patch(&key) + .body(json::to_string(&patch)?) + .header("content-type", "application/json-patch+json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + let res = http.key_get(&key).send().await?; + + let json = res.json::().await?; + + assert_eq!(json, json!({ "a": 4, "b": 5 })); + + Ok(()) +} + +#[tanu::test] +async fn get_json_patch_unsafe() -> eyre::Result<()> { + let key = random_key(); + + let http = Client::new(); + + let initial = json!({ + "a": { + "b": 1 + } + }); + + // create new blob + let res = http + .key_put(&key) + .body(json::to_string(&initial)?) + .header("huly-merge-strategy", "jsonpatch") + .header("content-type", "application/json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + let patch = json!([ + { "hop": "add", "path": "/a/b", "value": 0, "safe": false }, + { "hop": "inc", "path": "/a/c", "value": 1, "safe": false } + ]); + + let res = http + .key_patch(&key) + .body(json::to_string(&patch)?) + .header("content-type", "application/json-patch+json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + let res = http.key_get(&key).send().await?; + + let json = res.json::().await?; + + assert_eq!(json, json!({ "a": { "b": 0, "c": 1 }})); + + Ok(()) +} + +#[tanu::test] +async fn get_json_patch_safe() -> eyre::Result<()> { + let key = random_key(); + + let http = Client::new(); + + let initial = json!({ + "a": { + "b": 1 + } + }); + + // create new blob + let res = http + .key_put(&key) + .body(json::to_string(&initial)?) + .header("huly-merge-strategy", "jsonpatch") + .header("content-type", "application/json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + let patch = json!([ + { "hop": "add", "path": "/a/b", "value": 0, "safe": true }, + { "hop": "inc", "path": "/a/c", "value": 1, "safe": true } + ]); + + let res = http + .key_patch(&key) + .body(json::to_string(&patch)?) + .header("content-type", "application/json-patch+json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + let res = http.key_get(&key).send().await?; + + let json = res.json::().await?; + + assert_eq!(json, json!({ "a": { "b": 1 }})); + + Ok(()) +} + +#[derive(PartialEq, Eq)] +pub enum IfMatch { + ETag, + Some(&'static str), + None, +} + +#[tanu::test(1, IfMatch::None, StatusCode::CREATED)] +#[tanu::test(2, IfMatch::ETag, StatusCode::CREATED)] +#[tanu::test(3, IfMatch::Some("*"), StatusCode::CREATED)] +#[tanu::test(4, IfMatch::Some("\"unknown\""), StatusCode::PRECONDITION_FAILED)] +pub async fn put_and_patch_conditional( + _: usize, + if_match: IfMatch, + status: StatusCode, +) -> eyre::Result<()> { + let key = random_key(); + let initial = random_text(1024); + let patch = random_text(1024); + + let http = Client::new(); + + // create new blob + let res = http.key_put(&key).body(initial.clone()).send().await?; + check!(res.status().is_success()); + + // check content + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + let etag = res.header("etag").expect("ETag not found"); + + let mut req = http.key_patch(&key).body(patch.clone()); + + req = match if_match { + IfMatch::ETag => req.header("If-Match", etag), + IfMatch::Some(etag) => req.header("If-Match", etag), + IfMatch::None => req, + }; + + let res = req.send().await?; + check_eq!(res.status(), status); + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + + if status.is_success() { + check_ne!(res.header("etag"), Some(etag)); + } else { + check_eq!(res.header("etag"), Some(etag)); + } + + Ok(()) +} + +#[tanu::test] +pub async fn put_and_patch_json_err() -> eyre::Result<()> { + let key = random_key(); + + let http = Client::new(); + + let initial = json!({ + "a": 1 + }); + + // create new blob + let res = http + .key_put(&key) + .body(json::to_string(&initial)?) + .header("huly-merge-strategy", "jsonpatch") + .header("content-type", "application/json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + let patch = json!([ + { "hop": "add", "path": "/a/b/c", "value": 0, "safe": false }, + ]); + + let res = http + .key_patch(&key) + .body(json::to_string(&patch)?) + .header("content-type", "application/json-patch+json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + + let json = res.json::().await?; + assert_eq!(json, json!({ "a": 1 })); + + Ok(()) +} + +#[tanu::test(1)] +#[tanu::test(2)] +#[tanu::test(5)] +#[tanu::test(10)] +#[tanu::test(50)] +pub async fn put_and_patch_concurrent(count: usize) -> eyre::Result<()> { + let key = random_key(); + + let http = Client::new(); + + let initial = json!({ + "a": 0 + }); + + // create new blob + let res = http + .key_put(&key) + .body(json::to_string(&initial)?) + .header("huly-merge-strategy", "jsonpatch") + .header("content-type", "application/json") + .send() + .await?; + + check!(res.status().is_success(), "{:#?}", res); + + let patch = json!([ + { "hop": "inc", "path": "/a", "value": 1 }, + ]); + + let body: String = json::to_string(&patch)?; + + let futures: Vec<_> = (0..count) + .map(|_| { + let key = key.clone(); + let body = body.clone(); + let http = http.clone(); + + async move { + http.key_patch(&key) + .body(body) + .header("content-type", "application/json-patch+json") + .send() + .await + } + }) + .collect(); + + let results = join_all(futures).await; + for result in results.iter() { + assert!(result.is_ok()) + } + + let res = http.key_get(&key).send().await?; + check!(res.status().is_success(), "{:#?}", res); + let json = res.json::().await?; + assert_eq!(json, json!({ "a": count })); + + Ok(()) +} diff --git a/foundations/hulylake/tests/src/put.rs b/foundations/hulylake/tests/src/put.rs new file mode 100644 index 0000000000..e0597cdb34 --- /dev/null +++ b/foundations/hulylake/tests/src/put.rs @@ -0,0 +1,414 @@ +use hulyrs::StatusCode; +use tanu::{ + check, check_eq, eyre, + http::{self, Client}, +}; + +use crate::util::*; + +#[tanu::test] +pub async fn put_new_deduplicated_no_multipart() -> eyre::Result<()> { + let text = random_text(1024); + + let http = Client::new(); + + let res = http + .key_put(&random_key()) + .body(text.clone()) + .send() + .await?; + check!(res.status().is_success()); + check_eq!(None, res.header("huly-deduplicated")); + + let res = http + .key_put(&random_key()) + .body(text.clone()) + .send() + .await?; + check!(res.status().is_success()); + check_eq!(Some("true"), res.header("huly-deduplicated")); + + Ok(()) +} + +#[tanu::test(1)] +#[tanu::test(2)] +#[tanu::test(10)] +#[tanu::test(100)] +#[tanu::test(1024)] +#[tanu::test(1024 * 10)] +pub async fn put_new_sized_no_multipart(size: usize) -> eyre::Result<()> { + let key = random_key(); + let text = random_text(size); + + let http = Client::new(); + + // create new blob + let res = http + .key_put(&key) + .body(text.clone()) + .header(http::header::CONTENT_TYPE, "text/plain") + .send() + .await?; + + check!(res.status().is_success()); + check!(res.headers().get(http::header::ETAG).is_some()); + check_eq!(None, res.header("huly-parts-count")); + + // check content + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + check_eq!( + "text/plain", + res.headers().get(http::header::CONTENT_TYPE).unwrap() + ); + check_eq!( + Some(size.to_string().as_str()), + res.header("content-length") + ); + check_eq!(text, res.text().await?); + + Ok(()) +} + +#[tanu::test] +pub async fn put_new_sized_multipart() -> eyre::Result<()> { + let text = random_text(1024 * 1024 * 5); // above multipart threshold + let http = Client::new(); + + let key1 = random_key(); + + let res = http.key_put(&key1).body(text.clone()).send().await?; + check!(res.status().is_success()); + + check!(res.status().is_success()); + check_eq!(Some("1"), res.header("huly-s3-parts-count")); + + let res = http.key_get(&key1).send().await?; + check!(res.status().is_success()); + check_eq!( + Some(text.len().to_string().as_str()), + res.header("content-length") + ); + check_eq!(text, res.text().await?); + + let key2 = random_key(); + + let res = http.key_put(&key2).body(text.clone()).send().await?; + check!(res.status().is_success()); + check_eq!(None, res.header("huly-s3-parts-count")); + check_eq!(Some("true"), res.header("huly-deduplicated")); + + let res = http.key_get(&key2).send().await?; + check!(res.status().is_success()); + check_eq!( + Some(text.len().to_string().as_str()), + res.header("content-length") + ); + check_eq!(text, res.text().await?); + + Ok(()) +} + +#[tanu::test] +pub async fn put_existing() -> eyre::Result<()> { + let key = random_key(); + let text1 = random_text(1024); + let text2 = random_text(1024 * 10); + + let http = Client::new(); + + // create new blob + let res = http.key_put(&key).body(text1.clone()).send().await?; + check!(res.status().is_success()); + + // update content + let res = http.key_put(&key).body(text2.clone()).send().await?; + check!(res.status().is_success()); + check!(res.headers().get(http::header::ETAG).is_some()); + + // check content + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + check_eq!(text2, res.text().await?); + + Ok(()) +} + +#[tanu::test(1)] +#[tanu::test(1024)] +#[tanu::test(10 * 1024)] +#[tanu::test(1 * 1024 * 1024)] +#[tanu::test(10 * 1024 * 1024)] +pub async fn put_with_length(length: usize) -> eyre::Result<()> { + let key = random_key(); + let body = random_body(length); + + let http = Client::new(); + + // create new blob + let res = http.key_put(&key).body(body).send().await?; + check!(res.status().is_success()); + + // check content + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + check_eq!( + length, + res.headers() + .get(http::header::CONTENT_LENGTH) + .unwrap() + .to_str()? + .parse::()?, + "Invalid content length" + ); + + Ok(()) +} + +#[tanu::test] +pub async fn put_with_zero_length() -> eyre::Result<()> { + let key = random_key(); + let body = random_body(0); + + let http = Client::new(); + + // create new blob + let res = http.key_put(&key).body(body).send().await?; + check!(!res.status().is_success()); + check_eq!(http::StatusCode::BAD_REQUEST, res.status()); + + Ok(()) +} + +#[tanu::test("huly-header-header", "header", "value1")] +#[tanu::test("huly-header-HeAdEr", "header", "value2")] +#[tanu::test("HULY-HEADER-HEADER", "header", "value3")] +#[tanu::test("HULY-HEADER-HEADER", "HEADER", "value4")] +pub async fn put_with_header_case( + req_header: &str, + res_header: &str, + value: &str, +) -> eyre::Result<()> { + let key = random_key(); + let body = random_body(1024 * 10); + + let http = Client::new(); + + // create new blob + let res = http + .key_put(&key) + .body(body) + .header(req_header, value) + .send() + .await?; + check!(res.status().is_success()); + + // check header is returned + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + check_eq!(Some(value), res.header(res_header)); + + Ok(()) +} + +#[tanu::test] +pub async fn put_with_headers() -> eyre::Result<()> { + let key = random_key(); + let body = random_body(1024 * 10); + + let http = Client::new(); + + // create new blob + let res = http + .key_put(&key) + .body(body) + .header("huly-header-header1", "foo") + .header("huly-header-header2", "bar") + .header("huly-meta-meta1", "baz") + .header("content-type", "application/json") + .send() + .await?; + check!(res.status().is_success()); + + // check headers are returned + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + check_eq!(Some("foo"), res.header("header1")); + check_eq!(Some("bar"), res.header("header2")); + check_eq!(Some("application/json"), res.header("content-type")); + check_eq!(None, res.header("meta1")); + + Ok(()) +} + +#[tanu::test] +pub async fn put_with_meta() -> eyre::Result<()> { + let key = random_key(); + let body = random_body(1024 * 10); + + let http = Client::new(); + + // create new blob + let res = http + .key_put(&key) + .body(body) + .header("huly-meta-meta1", "foo") + .header("huly-meta-meta2", "bar") + .send() + .await?; + check!(res.status().is_success()); + + // check meta is private + let res = http.key_get(&key).send().await?; + check!(res.status().is_success()); + check!(res.headers().get("meta1").is_none()); + check!(res.headers().get("meta2").is_none()); + + Ok(()) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Body { + Random(usize), + Text(&'static str), +} + +#[tanu::test(1, None, None, Body::Text("{}"), StatusCode::CREATED)] +#[tanu::test(2, Some("invalid"), None, Body::Text("{}"), StatusCode::BAD_REQUEST)] +#[tanu::test(3, Some("jsonpatch"), None, Body::Text("{}"), StatusCode::BAD_REQUEST)] +#[tanu::test( + 4, + Some("jsonpatch"), + Some("application/xml"), + Body::Text("{}"), + StatusCode::BAD_REQUEST +)] +#[tanu::test( + 5, + Some("jsonpatch"), + Some("application/json"), + Body::Text("{"), + StatusCode::BAD_REQUEST +)] +#[tanu::test( + 6, + Some("jsonpatch"), + Some("application/json"), + Body::Random(1024 * 101), + StatusCode::BAD_REQUEST +)] +#[tanu::test( + 7, + Some("jsonpatch"), + Some("application/json"), + Body::Text("{}"), + StatusCode::CREATED +)] + +pub async fn put_merge_patch( + _: usize, + strategy: Option<&str>, + content_type: Option<&str>, + body: Body, + status: StatusCode, +) -> eyre::Result<()> { + let http = Client::new(); + let key = random_key(); + + let mut req = http.key_put(&key); + + match body { + Body::Random(size) => { + req = req.body(random_body(size)); + } + Body::Text(text) => { + req = req.body(text.to_string()); + } + } + + if let Some(strategy) = strategy { + req = req.header("huly-merge-strategy", strategy); + } + + if let Some(content_type) = content_type { + req = req.header("content-type", content_type); + } + + let res = req.send().await?; + + check!(res.status() == status); + + Ok(()) +} + +#[derive(PartialEq, Eq)] +pub enum Condition { + ETag, + IfMatch(&'static str), + IfNoneMatch(&'static str), +} + +#[tanu::test(1, Condition::ETag, StatusCode::CREATED)] +#[tanu::test(2, Condition::IfMatch("*"), StatusCode::PRECONDITION_FAILED)] +#[tanu::test(3, Condition::IfNoneMatch("*"), StatusCode::CREATED)] +#[tanu::test(4, Condition::IfNoneMatch("\"unknown\""), StatusCode::CREATED)] +pub async fn put_conditional_create( + _: usize, + condition: Condition, + status: StatusCode, +) -> eyre::Result<()> { + let key = random_key(); + let body = random_text(1024); + + let http = Client::new(); + + let mut req = http.key_put(&key).body(body.clone()); + + req = match condition { + Condition::ETag => req, + Condition::IfMatch(etag) => req.header("If-Match", etag), + Condition::IfNoneMatch(etag) => req.header("If-None-Match", etag), + }; + + let res = req.send().await?; + check_eq!(res.status(), status); + + Ok(()) +} + +#[tanu::test(1, Condition::ETag, StatusCode::CREATED)] +#[tanu::test(2, Condition::IfMatch("*"), StatusCode::CREATED)] +#[tanu::test(3, Condition::IfNoneMatch("*"), StatusCode::PRECONDITION_FAILED)] +#[tanu::test( + 4, + Condition::IfNoneMatch("\"unknown\""), + StatusCode::PRECONDITION_FAILED +)] +pub async fn put_conditional_update( + _: usize, + condition: Condition, + status: StatusCode, +) -> eyre::Result<()> { + let key = random_key(); + let body = random_text(1024); + + let http = Client::new(); + + let res = http.key_put(&key).body(body.clone()).send().await?; + check!(res.status().is_success()); + let etag = res.header("etag").expect("ETag not found"); + + let mut req = http.key_put(&key).body(body.clone()); + + req = match condition { + Condition::ETag => req.header("If-Match", etag), + Condition::IfMatch(etag) => req.header("If-Match", etag), + Condition::IfNoneMatch(etag) => req.header("If-None-Match", etag), + }; + + let res = req.send().await?; + check_eq!(res.status(), status); + + Ok(()) +} diff --git a/foundations/hulylake/tests/src/sanity.rs b/foundations/hulylake/tests/src/sanity.rs new file mode 100644 index 0000000000..92a2fd257a --- /dev/null +++ b/foundations/hulylake/tests/src/sanity.rs @@ -0,0 +1,14 @@ +use crate::config::CONFIG; +use tanu::{check, check_eq, eyre, http::Client}; + +#[tanu::test] +async fn status_is_ok() -> eyre::Result<()> { + let http = Client::new(); + let res = http + .get(format!("{}/status", CONFIG.base_url)) + .send() + .await?; + check!(res.status().is_success()); + check_eq!("ok", res.text().await?); + Ok(()) +} diff --git a/foundations/hulylake/tests/src/util.rs b/foundations/hulylake/tests/src/util.rs new file mode 100644 index 0000000000..f318812894 --- /dev/null +++ b/foundations/hulylake/tests/src/util.rs @@ -0,0 +1,98 @@ +use crate::config::CONFIG; +use rand::{Rng, RngCore}; +use reqwest::Body; +use secrecy::ExposeSecret; +use tanu::http::{Client, Method, RequestBuilder}; + +pub trait ClientExt { + fn key_head(&self, key: &str) -> RequestBuilder; + fn key_get(&self, key: &str) -> RequestBuilder; + fn key_put(&self, key: &str) -> RequestBuilder; + fn key_patch(&self, key: &str) -> RequestBuilder; + fn key_delete(&self, key: &str) -> RequestBuilder; + fn request(&self, method: &Method, path: &str) -> RequestBuilder; +} + +pub fn path(key: &str) -> String { + format!("{}/api/{}/{key}", CONFIG.base_url, CONFIG.workspace) +} + +impl ClientExt for Client { + fn request(&self, method: &Method, path: &str) -> RequestBuilder { + match *method { + Method::HEAD => self.head(path), + Method::GET => self.get(path), + Method::PUT => self.put(path), + Method::POST => self.post(path), + Method::DELETE => self.delete(path), + _ => panic!("unsupported method"), + } + } + + fn key_head(&self, key: &str) -> RequestBuilder { + self.head(path(key)) + .bearer_auth(CONFIG.token_valid.expose_secret()) + } + + fn key_get(&self, key: &str) -> RequestBuilder { + self.get(path(key)) + .bearer_auth(CONFIG.token_valid.expose_secret()) + } + + fn key_put(&self, key: &str) -> RequestBuilder { + self.put(path(key)) + .bearer_auth(CONFIG.token_valid.expose_secret()) + } + + fn key_patch(&self, key: &str) -> RequestBuilder { + self.patch(path(key)) + .bearer_auth(CONFIG.token_valid.expose_secret()) + } + + fn key_delete(&self, key: &str) -> RequestBuilder { + self.get(path(key)) + .bearer_auth(CONFIG.token_valid.expose_secret()) + } +} + +pub trait ResponseExt { + fn header(&self, key: &str) -> Option<&str>; +} + +impl ResponseExt for tanu::http::Response { + fn header(&self, key: &str) -> Option<&str> { + self.headers().get(key).and_then(|v| v.to_str().ok()) + } +} + +pub fn random_text(length: usize) -> String { + let mut rng = rand::rng(); + let charset: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 "; + + let text: String = (0..length) + .map(|_| { + let idx = rng.random_range(0..charset.len()); + charset[idx] as char + }) + .collect(); + + text +} + +pub fn random_body(size: usize) -> Body { + let mut rng = rand::rng(); + let mut bytes = vec![0u8; size]; + + rng.fill_bytes(&mut bytes); + + Body::from(bytes) +} + +pub fn random_key() -> String { + let mut rng = rand::rng(); + let mut bytes = vec![0u8; 16]; + + rng.fill_bytes(&mut bytes); + + hex::encode(bytes) +} diff --git a/foundations/hulylake/tests/tanu.toml b/foundations/hulylake/tests/tanu.toml new file mode 100644 index 0000000000..e9bdb5694a --- /dev/null +++ b/foundations/hulylake/tests/tanu.toml @@ -0,0 +1,3 @@ +[[projects]] +name = "dev" +base_url = "http://localhost:8096" diff --git a/foundations/hulypulse/.github/workflows/build.yml b/foundations/hulypulse/.github/workflows/build.yml new file mode 100644 index 0000000000..387987e13c --- /dev/null +++ b/foundations/hulypulse/.github/workflows/build.yml @@ -0,0 +1,34 @@ +name: Hulypulse + +on: + workflow_dispatch: + push: + tags: + - 'v*.*.*' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log to registry + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKER_USER }} + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + + - run: echo VERSION=$(grep '^version =' Cargo.toml | cut -d '"' -f 2) >> $GITHUB_ENV + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and Push + uses: docker/build-push-action@v6 + with: + file: Dockerfile + push: true + tags: "${{ vars.DOCKER_USER }}/service_hulypulse:${{ env.VERSION }},${{ vars.DOCKER_USER }}/service_hulypulse:latest" + platforms: linux/amd64,linux/arm64 diff --git a/foundations/hulypulse/.gitignore b/foundations/hulypulse/.gitignore new file mode 100644 index 0000000000..543c428394 --- /dev/null +++ b/foundations/hulypulse/.gitignore @@ -0,0 +1,19 @@ +/off +/target +/scripts/other +/scripts/off +/scripts/typing-test.sh +/scripts/TEST_LLEOTOKEN.html +Justfile +commit.sh +/src/GO.sh +/src/GOT.sh +GO.sh +TEST.sh +TEST_WS.sh +DROP_DB.sh +TODO.txt +DOCKER.sh +/lleo +/client +/scripts diff --git a/foundations/hulypulse/Cargo.lock b/foundations/hulypulse/Cargo.lock new file mode 100644 index 0000000000..1d45a58ccd --- /dev/null +++ b/foundations/hulypulse/Cargo.lock @@ -0,0 +1,4307 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" +dependencies = [ + "actix-macros", + "actix-rt", + "actix_derive", + "bitflags 2.9.4", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot 0.12.4", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-cors" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec", +] + +[[package]] +name = "actix-http" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "base64 0.22.1", + "bitflags 2.9.4", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash", + "futures-core", + "h2", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-tls" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "impl-more", + "pin-project-lite", + "tokio", + "tokio-rustls 0.23.4", + "tokio-util", + "tracing", + "webpki-roots 0.22.6", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.5.10", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-actors" +version = "4.3.1+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes", + "bytestring", + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "actix-ws" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a1fb4f9f2794b0aadaf2ba5f14a6f034c7e86957b458c506a8cb75953f2d99" +dependencies = [ + "actix-codec", + "actix-http", + "actix-web", + "bytestring", + "futures-core", + "tokio", +] + +[[package]] +name = "actix_derive" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-tungstenite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f" +dependencies = [ + "atomic-waker", + "futures-core", + "futures-io", + "futures-task", + "futures-util", + "log", + "pin-project-lite", + "tungstenite 0.27.0", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borrow-or-share" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytestring" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "config" +version = "0.15.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e549344080374f9b32ed41bf3b6b57885ff6a289367b3dbc10eea8acc1918" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde-untagged", + "serde_core", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.11", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fluent-uri" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "governor" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" +dependencies = [ + "cfg-if", + "dashmap", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.3", + "hashbrown 0.15.5", + "nonzero_ext", + "parking_lot 0.12.4", + "portable-atomic", + "quanta", + "rand 0.9.2", + "smallvec", + "spinning_top", + "web-time", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.11.4", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hulypulse" +version = "0.2.1" +dependencies = [ + "actix", + "actix-cors", + "actix-web", + "actix-web-actors", + "actix-ws", + "anyhow", + "config", + "futures-util", + "hex", + "hulyrs", + "md5", + "redis", + "regorus", + "secrecy", + "serde", + "serde_json", + "serde_with", + "size", + "strum", + "strum_macros", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "hulyrs" +version = "0.1.0" +source = "git+https://github.com/hcengineering/hulyrs.git#3684385152b5022a2ef2c44a4e99c561e965de57" +dependencies = [ + "actix-web", + "bytes", + "chrono", + "config", + "derive_builder", + "futures", + "governor", + "itoa", + "jsonwebtoken", + "num-traits", + "rand 0.9.2", + "reqwest", + "reqwest-middleware", + "reqwest-ratelimit", + "reqwest-retry", + "reqwest-websocket", + "ryu", + "secrecy", + "serde", + "serde_json", + "serde_with", + "strum", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio_with_wasm", + "tracing", + "url", + "uuid", + "wasmtimer", +] + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.3.1", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.3.1", + "hyper", + "hyper-util", + "rustls 0.23.32", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots 1.0.2", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonschema" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d" +dependencies = [ + "ahash", + "base64 0.22.1", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "idna", + "itoa", + "num-cmp", + "num-traits", + "once_cell", + "percent-encoding", + "referencing", + "regex", + "regex-syntax", + "serde", + "serde_json", + "uuid-simd", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring 0.17.14", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "msvc_spectre_libs" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29e871a9861f3664f18b7e04e9301d4edd55090c2dadb4b1c602e26ab32b1f5b" +dependencies = [ + "cc", +] + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.11", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.17", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" +dependencies = [ + "memchr", + "thiserror 2.0.17", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.32", + "socket2 0.6.0", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring 0.17.14", + "rustc-hash", + "rustls 0.23.32", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.0", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "redis" +version = "0.32.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "ryu", + "sha1_smol", + "socket2 0.6.0", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "referencing" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e" +dependencies = [ + "ahash", + "fluent-uri", + "once_cell", + "parking_lot 0.12.4", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regex" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "regorus" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee058fce2fefa4eb9364b0a514296c70235f0f29bb92ec2c0d24766b837f08aa" +dependencies = [ + "anyhow", + "chrono", + "chrono-tz", + "data-encoding", + "globset", + "jsonschema", + "lazy_static", + "msvc_spectre_libs", + "rand 0.9.2", + "regex", + "scientific", + "semver", + "serde", + "serde_json", + "serde_yaml", + "thiserror 2.0.17", + "url", + "uuid", +] + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.32", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.2", +] + +[[package]] +name = "reqwest-middleware" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +dependencies = [ + "anyhow", + "async-trait", + "http 1.3.1", + "reqwest", + "serde", + "thiserror 1.0.69", + "tower-service", +] + +[[package]] +name = "reqwest-ratelimit" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8fff0d8036f23dcad6c27605ca3baa8ae3867438d0a8b34072f40f6c8bf628" +dependencies = [ + "async-trait", + "http 1.3.1", + "reqwest", + "reqwest-middleware", +] + +[[package]] +name = "reqwest-retry" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c73e4195a6bfbcb174b790d9b3407ab90646976c55de58a6515da25d851178" +dependencies = [ + "anyhow", + "async-trait", + "futures", + "getrandom 0.2.16", + "http 1.3.1", + "hyper", + "parking_lot 0.11.2", + "reqwest", + "reqwest-middleware", + "retry-policies", + "thiserror 1.0.69", + "tokio", + "tracing", + "wasm-timer", +] + +[[package]] +name = "reqwest-websocket" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd5f79b25f7f17a62cc9337108974431a66ae5a723ac0d9fe78ac1cce2027720" +dependencies = [ + "async-tungstenite", + "bytes", + "futures-util", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", + "tungstenite 0.27.0", + "web-sys", +] + +[[package]] +name = "retry-policies" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.9.4", + "serde", + "serde_derive", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "once_cell", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.103.7", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.1", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scientific" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" +dependencies = [ + "scientific-macro", +] + +[[package]] +name = "scientific-macro" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ee4885492bb655bfa05d039cd9163eb8fe9f79ddebf00ca23a1637510c2fd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.4", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.11.4", + "schemars 0.9.0", + "schemars 1.0.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.11.4", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "size" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b" +dependencies = [ + "serde", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot 0.12.4", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.32", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "rustls 0.22.4", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tungstenite 0.21.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio_with_wasm" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dfba9b946459940fb564dcf576631074cdfb0bfe4c962acd4c31f0dca7897e6" +dependencies = [ + "js-sys", + "tokio", + "tokio_with_wasm_proc", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "tokio_with_wasm_proc" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e04c1865c281139e5ccf633cb9f76ffdaabeebfe53b703984cf82878e2aabb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.22.4", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +dependencies = [ + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "rand 0.9.2", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.12.4", + "pin-utils", + "slab", + "wasm-bindgen", +] + +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.4", +] + +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yaml-rust2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/foundations/hulypulse/Cargo.toml b/foundations/hulypulse/Cargo.toml new file mode 100644 index 0000000000..611fb59db9 --- /dev/null +++ b/foundations/hulypulse/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "hulypulse" +version = "0.2.1" +edition = "2024" + +[dependencies] +tokio = { version = "1", features = ["full", "macros", "rt-multi-thread"] } +tracing = "0.1.41" +tracing-subscriber = "0.3.19" +anyhow = "1.0.97" +config = "0.15.4" +serde = { version = "1.0.219", features = ["derive"] } +actix = "0.13.5" +actix-web = "4.10.2" +actix-cors = "0.7.1" +actix-web-actors = "4.2.0" +redis = { version = "=0.32.5", features = ["aio", "tokio-comp", "sentinel"] } +md5 = "0.8.0" +serde_with = "3" +url = "2" +size = { version = "0.5.0", features = ["serde"] } +uuid = { version = "1.7", features = ["v4", "serde"] } +hex = "0.4.3" +serde_json = "1.0" +hulyrs = { git = "https://github.com/hcengineering/hulyrs.git", features = [ + "actix", +] } +secrecy = "0.10.3" +tokio-stream = "0.1" +strum = { version = "0.27.2", features = ["derive"] } +strum_macros = "0.27.2" +regorus = "0.5.0" +actix-ws = "0.3.0" +futures-util = "0.3" + +[[bin]] +name = "hulypulse" +path = "src/main.rs" + +[dev-dependencies] +tokio-tungstenite = { version = "0.21", default-features = false, features = [ + "rustls-tls-native-roots", + "connect", +] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +futures-util = "0.3" +serde_json = "1" +uuid = "1" diff --git a/foundations/hulypulse/Dockerfile b/foundations/hulypulse/Dockerfile new file mode 100644 index 0000000000..7d80417bf7 --- /dev/null +++ b/foundations/hulypulse/Dockerfile @@ -0,0 +1,35 @@ +FROM --platform=$BUILDPLATFORM rust:1.88 AS builder +ARG TARGETPLATFORM + +WORKDIR /tmp/build + +COPY . . + +RUN \ + if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ + cargo build --release --target=x86_64-unknown-linux-gnu ; \ + elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ + apt-get update && apt-get install -y \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu \ + libc6-dev-arm64-cross \ + && rm -rf /var/lib/apt/lists/* ; \ +\ + rustup target add aarch64-unknown-linux-gnu ; \ +\ + export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc ; \ + export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ ; \ + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc ; \ +\ + cargo build --release --target=aarch64-unknown-linux-gnu ; \ + else \ + echo "Unexpected target platform: $TARGETPLATFORM" && exit 1 ; \ + fi + +FROM debian:12-slim + +ARG TARGET +COPY --from=builder /tmp/build/target/*/release/hulypulse /usr/local/bin/hulypulse +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +ENTRYPOINT ["/usr/local/bin/hulypulse"] diff --git a/foundations/hulypulse/LICENSE b/foundations/hulypulse/LICENSE new file mode 100644 index 0000000000..e48e096345 --- /dev/null +++ b/foundations/hulypulse/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/foundations/hulypulse/README.md b/foundations/hulypulse/README.md new file mode 100644 index 0000000000..3b4a447a9f --- /dev/null +++ b/foundations/hulypulse/README.md @@ -0,0 +1,243 @@ +# Hulypulse + +Hulypulse is a service that enables clients to share information on a “whiteboard”. Clients connected to the same “whiteboard” see data provided by other clients to the whiteboard. + +The service is exposed as REST and WebSocket API. + +**Usage scenarios:** + +- user presence in a document +- user is “typing” event +- user cursor position in editor or drawing board +- service posts a process status + +## Key + +Key is a string that consists of one or multiple segments separated by ‘/’. Example: foo/bar/baz. +Key may not end with ‘/’ +Segment may not contain special characters (‘*’, ‘?’, ‘[’, ‘]’,‘\’,‘\x00..\xF1’,‘\x7F’,‘"’,‘'’) +Segment may not be empty +Key segment may be private (prefixed with ‘$’) + + Query + +May not contain special characters (‘*’, ‘?’, ‘[’, ‘]’,‘\’,‘\x00..\xF1’,‘\x7F’,‘"’,‘'’) +It is possible to use prefix, for listings / subscriptions (prefix ends with segment separator ‘/’) + +- GET/SUBSCRIBE/.. a/b → single key +- GET/SUBSCRIBE/.. a/b/c/ → multiple + + If multiple + +select all keys starting with prefix +skip keys, containing private segments to the right from the prefix + + example + +- 1. /a/b/$c/$d, 2. /a/b/c, 3. /a/b/$c, 4. /a/b/$c/$d/e +- / → [2] +- /a/b/ → [2] +- /a/b/$c/ → [3] +- /a/b/$c/$d/ → [4] +- /a/b/$c/$d → [1] + + +## Data +“Data” is an arbitrary JSON document. +Size of data is limited to some reasonable size + +## HTTP API + +```GET /status``` - server status and websockets count +- Answer: `{"status":"OK","websockets":2}` + + +```PUT /{workspace}/{key}``` - Save key +- Input + - Body - data + - Content-Type: application/json + - Content-Length: optional + - Headers: TTL or absolute expiration time + - `HULY-TTL` — autodelete in N seconds + - or `HULY-EXPIRE-AT` — autodelete in UnixTime + - default max_ttl = 3600 (settings in config/default.toml) + - Conditional Headers: + - `If-Match: *` — update only if the key exists + - `If-Match: ` — update only if current value's MD5 matches + - `If-None-Match: *` — insert only if the key does not exist +- Output + - Status: + - `201` if inserted with `If-None-Match: *` + - `204` on successful insert or update + - `412` if the condition is not met + - `400` if headers are invalid + - Body: `DONE` + +```DELETE /{workspace}/{key}``` - Delete key +- Output + - Status: `204 No content`, no body + - `404 Not Found` if nothing to do + +```GET /{workspace}/{key}``` - Read one key +- Output + - Status 200 + - Content-type: application/json + - Header: `Etag: ` + - Body: + - workspace (copy of input) + - key (copy of input) + - data (copy of input) + - expiresAt / TTL (copy of input, optional) + - etag + +```GET /{workspace}/{key}/``` - Read array of keys +- Output + - Status 200 + - Content-type: application/json + - Body (array): + - [{"key","data","ttl","etag"}, ...] + +## WebSocket API + +**Client to Server** + +```PUT``` + - type: "put" + - correlation id (optional) + - key: + - “workspace/foo/bar“ - shared key + - “workspace/foo/bar/$/secret“ - secret key + - data + ** time control (optional) ** + - `TTL` — autodelete in N seconds + - `ExpireAt` — autodelete in UnixTime + - or default max_ttl = 3600 (settings in config/default.toml) + ** Conditional (optional) ** + - `ifMatch: *` — update only if the key exists + - `ifMatch: ` — update only if current value's MD5 matches + - `ifNoneMatch: *` — insert only if the key does not exist + +- Answer: `{"action":"put","correlation":"abc123","result":"OK"}` + + +```GET``` + - type: "get" + - correlation id (optional) + - key: + - “workspace/foo/bar“ - one shared key + - “workspace/foo/bar/$/secret“ - one secret key + +- Answer: `{"action":"get","result":{"data":"hello","etag":"5d41402abc4b2a76b9719d911017c592","ttl":3599,"key":"00000000-0000-0000-0000-000000000001/foo/bar"}}` + + +```LIST``` + - type: "list" + - correlation id (optional) + - key: + - “workspace/foo/bar/“ - keys from public space + - “workspace/foo/bar/$/secret/“ - keys from secret space + +- Answer: `{"action":"list","result":[{"data":"hello 1","etag":"df0649bc4f1be901c85b6183091c1d83","ttl":3570,"key":"00000000-0000-0000-0000-000000000001/foo/bar1"},{"data":"hello 2","etag":"bb21ec8394b75795622f61613a777a8b","ttl":3555,"key":"00000000-0000-0000-0000-000000000001/foo/bar2"}]}` + + +```DELETE``` + - type: "delete" + - correlation id (optional) + - key: “workspace/foo/bar“ + ** Conditional (optional) ** + - `ifMatch: ` — delete only if current value's MD5 matches + - `ifMatch: *` — return error if key does not exist + +- Answer: `{"action":"delete","result":"OK"}` + + +```SUBSCRIBE``` + type: "sub" + key: + - “workspace/foo/bar“ - subscribe one shared key + - “workspace/foo/bar/“ - subscribe all keys started with + - “workspace/foo/bar/$/my_secret“ - subscribe one secret key + - “workspace/foo/bar/$/my_secret/“ - subscribe all keys started with secret + +- Answer: `{"action":"sub","result":"OK"}` + + +```UNSUBSCRIBE``` + - type: "unsub" + - key: + - “workspace/foo/bar“ - unsubscribe subscribed key + - “*“ - unsubscribe all + +- Answer: `{"action":"unsub","result":"OK"}` + +```MY SUBSCRIBES``` + - type: "sublist" + +- Answer: `{"action":"list","result":["00000000-0000-0000-0000-000000000001/foo/bar1","00000000-0000-0000-0000-000000000001/foo/bar2"]}` + +```INFO``` + - type: "info" + +- Answer: `{"db_mode":"memory","memory_info":"1231 keys, 80345 bytes","status":"OK","websockets":164}` + + +** Server to Client ** subscribed events: + + - `{"message":"Set","key":"00000000-0000-0000-0000-000000000001/foo/bar","value":"hello"}` + + - `{"message":"Expired","key":"00000000-0000-0000-0000-000000000001/foo/bar"}` + + - `{"message":"Del","key":"00000000-0000-0000-0000-000000000001/foo/bar"}` + +## Special options in config/default.toml + - ```memory_mode = true``` Use native memory storage instead Redis + - ```no_authorization = true``` Don't check authorization + - ```max_size = 100``` Max value size in bytes + + +## Running + +Pre-build docker images is available at: hardcoreeng/service_hulypulse:{tag}. + +You can use the following command to run the image locally: +```bash +docker run -p 8095:8095 -it --rm hardcoreeng/service_hulypulse:{tag}" +``` + +If you want to run the service as a part of local huly development environment use the following command: +```bash + export HULY_REDIS_URLS="redis://huly.local:6379" + docker run --rm -it --network dev_default -p 8095:8095 hardcoreeng/service_hulypulse:{tag} +``` +This will run Hulypulse in the same network as the rest of huly services, and set the redis connection string to the one matching the local dev redis instance. + +You can then access hulypulse at http://localhost:8095. + + +## Authetication +Hulypulse uses bearer JWT token authetication. At the moment, it will accept any token signed by the hulypulse secret. The secret is set in the environment variable HULY_TOKEN_SECRET variable. + +## Configuration +The following environment variables are used to configure hulypulse: + - ```HULY_BIND_HOST```: host to bind the server to (default: 0.0.0.0) + - ```HULY_BIND_PORT```: port to bind the server to (default: 8094) + - ```HULY_TOKEN_SECRET```: secret used to sign JWT tokens (default: secret) + - ```HULY_REDIS_URLS```: redis connection string (default: redis://huly.local:6379) + - ```HULY_REDIS_PASSWORD```: redis password (default: "<invalid>") + - ```HULY_REDIS_MODE```: redis mode "direct" or "sentinel" (default: "direct") + - ```HULY_REDIS_SERVICE```: redis service (default: "mymaster") + - ```HULY_MAX_TTL```: maximum storage time (default: 3600) + - TODO: ```HULY_PAYLOAD_SIZE_LIMIT```: maximum size of the payload (default: 2Mb) + +## Todo (in no particular order) +- [ ] Optional value encryption +- [ ] Support for open telemetry +- [ ] Concurrency control for database migration (several instances of hulypulse are updated at the same time) +- [ ] TLS support +- [ ] Liveness/readiness probe endpoint + +## Contributing +Contributions are welcome! Please open an issue or a pull request if you have any suggestions or improvements. + +## License +This project is licensed under EPL-2.0 diff --git a/foundations/hulypulse/client/off/client.ts b/foundations/hulypulse/client/off/client.ts new file mode 100644 index 0000000000..5908dfe483 --- /dev/null +++ b/foundations/hulypulse/client/off/client.ts @@ -0,0 +1,271 @@ +// +// Copyright © 2024-2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// import { WebSocket } from 'ws'; // для Node <20 обязательно + +// Unknown: +// import { type Ref, concatLink } from '@hcengineering/core' +// import { getMetadata } from '@hcengineering/platform' + +// import { getCurrentEmployee, type Person } from '@hcengineering/contact' +// import presence from '@hcengineering/presence' +// import presentation from '@hcengineering/presentation' +// import { type Unsubscriber, get } from 'svelte/store' + +// import { myPresence, myData, isAnybodyInMyRoom, onPersonUpdate, onPersonLeave, onPersonData } from './store' +// import type { RoomPresence, MyDataItem } from './types' + +// interface PresenceMessage { +// id: Ref +// type: 'update' | 'remove' +// presence?: RoomPresence[] +// lastUpdate?: number +// } + +// interface DataMessage { +// type: 'data' +// sender: Ref +// topic: string +// data: any +// } + +// type IncomingMessage = PresenceMessage | DataMessage + + + +export class HulypulseClient implements Disposable { + private ws: WebSocket | null = null + private closed = false + private reconnectTimeout: number | undefined + private pingTimeout: number | undefined + private pingInterval: number | undefined + private readonly RECONNECT_INTERVAL = 1000 + private readonly PING_INTERVAL = 30 * 1000 + private readonly PING_TIMEOUT = 5 * 60 * 1000 + private readonly myDataThrottleInterval = 100 + // private readonly url: string | URL = 'ws://localhost:8095' + + // private presence: RoomPresence[] + private readonly myDataTimestamps = new Map() + // // private readonly myPresenceUnsub: Unsubscriber + // private readonly myDataUnsub: Unsubscriber + + constructor (private readonly url: string | URL) { + // this.presence = get(myPresence) + // this.myPresenceUnsub = myPresence.subscribe((presence) => { + // this.handlePresenceChanged(presence) + // }) + // this.myDataUnsub = myData.subscribe((data) => { + // this.handleMyDataChanged(data, false) + // }) + + this.connect() + } + + // Close the connection + close (): void { + console.log('Closing connection') + this.closed = true + clearTimeout(this.reconnectTimeout) + this.stopPing() + + // this.myPresenceUnsub() + // this.myDataUnsub() + + if (this.ws !== null) { + this.ws.close() + this.ws = null + } + } + + // Open the connection and reconnect if it fails + private connect (): void { + console.log('Connecting to WebSocket: ', this.url) + try { + const ws = new WebSocket(this.url) + console.log('WebSocket created: ', ws) + this.ws = ws + + ws.onopen = () => { + console.log('WebSocket.onopen') + if (this.ws !== ws) { + return + } + + this.handleConnect() + } + + ws.onclose = (event: CloseEvent) => { + console.log('WebSocket.onclose') + if (this.ws !== ws) { + ws.close() + return + } + + this.reconnect() + } + + ws.onmessage = (event: MessageEvent) => { + console.log('WebSocket.onmessage: ', event.data) + if (this.closed || this.ws !== ws) { + return + } + + this.handleMessage(event.data) + } + + ws.onerror = (event: Event) => { + console.log('client websocket error', event) + if (this.ws !== ws) { + return + } + } + } catch (err: any) { + console.error('WebSocket error', err) + this.reconnect() + } + } + + private startPing (): void { + console.log('Starting ping') + clearInterval(this.pingInterval) + this.pingInterval = window.setInterval(() => { + if (this.ws !== null && this.ws.readyState === WebSocket.OPEN) { + this.ws.send('ping') + } + clearTimeout(this.pingTimeout) + this.pingTimeout = window.setTimeout(() => { + if (this.ws !== null) { + console.log('no response from server') + clearInterval(this.pingInterval) + this.ws.close(1000) + } + }, this.PING_TIMEOUT) + }, this.PING_INTERVAL) + } + + private stopPing (): void { + console.log('Stopping ping') + clearInterval(this.pingInterval) + this.pingInterval = undefined + + clearTimeout(this.pingTimeout) + this.pingTimeout = undefined + } + + private reconnect (): void { + console.log('Reconnecting...') + clearTimeout(this.reconnectTimeout) + this.stopPing() + + if (!this.closed) { + this.reconnectTimeout = window.setTimeout(() => { + this.connect() + }, this.RECONNECT_INTERVAL) + } + } + + private handleConnect (): void { +// this.sendPresence(getCurrentEmployee(), this.presence) + this.startPing() +// this.handleMyDataChanged(get(myData), true) + } + + private handleMessage (data: string): void { + if (data === 'pong') { + clearTimeout(this.pingTimeout) + return + } + + try { + const message = JSON.parse(data); // as IncomingMessage + console.log('Received message', message); + // const message = JSON.parse(data) as IncomingMessage + // if (message.type === 'update' && message.presence !== undefined) { + // onPersonUpdate(message.id, message.presence ?? []) + // } else if (message.type === 'remove') { + // onPersonLeave(message.id) + // } else if (message.type === 'data') { + // onPersonData(message.sender, message.topic, message.data) + // } else { + // console.warn('Unknown message type', message) + // } + } catch (err: any) { + console.error('Error parsing message', err, data) + } + } + + // private handlePresenceChanged (presence: RoomPresence[]): void { + // this.presence = presence + // this.sendPresence(getCurrentEmployee(), this.presence) + // this.handleMyDataChanged(get(myData), true) + // } + + // private sendPresence (person: Ref, presence: RoomPresence[]): void { + // if (!this.closed && this.ws !== null && this.ws.readyState === WebSocket.OPEN) { + // const message: PresenceMessage = { id: person, type: 'update', presence } + // this.ws.send(JSON.stringify(message)) + // } + // } + + // private handleMyDataChanged (data: Map, forceSend: boolean): void { + // if (!isAnybodyInMyRoom()) { + // return + // } + // if (!this.closed && this.ws !== null && this.ws.readyState === WebSocket.OPEN) { + // for (const [topic, value] of data) { + // const lastSend = this.myDataTimestamps.get(topic) ?? 0 + // if (value.lastUpdated >= lastSend + this.myDataThrottleInterval || forceSend) { + // this.myDataTimestamps.set(topic, value.lastUpdated) + // const message: DataMessage = { + // sender: getCurrentEmployee(), + // type: 'data', + // topic, + // data: value.data + // } + // this.ws.send(JSON.stringify(message)) + // } + // } + // } + // } + + [Symbol.dispose] (): void { + this.close() + } +} + +export function connect (): HulypulseClient | undefined { + // const wsUuid = getMetadata(presentation.metadata.WorkspaceUuid) + // if (wsUuid === undefined) { + // console.warn('Workspace uuid is not defined') + // return undefined + // } + + // const token = getMetadata(presentation.metadata.Token) + + // const presenceUrl = getMetadata(presence.metadata.PresenceUrl) + // if (presenceUrl === undefined || presenceUrl === '') { + // console.warn('Presence URL is not defined') + // return undefined + // } + + // const url = new URL(concatLink(presenceUrl, wsUuid)) + // if (token !== undefined) { + // url.searchParams.set('token', token) + // } + + // return new HulypulseClient(url) + return new HulypulseClient("ws://localhost:8095") +} \ No newline at end of file diff --git a/foundations/hulypulse/policy.repo b/foundations/hulypulse/policy.repo new file mode 100644 index 0000000000..0888019a93 --- /dev/null +++ b/foundations/hulypulse/policy.repo @@ -0,0 +1,7 @@ +default permit = true + +#permit if { +# input.command == "Get" +# contains(input.key, "/typing/") +# input.claim.workspace == "00000000-0000-0000-0000-000000000001" +#} diff --git a/foundations/hulypulse/scripts/TEST.html b/foundations/hulypulse/scripts/TEST.html new file mode 100644 index 0000000000..b7f47d8964 --- /dev/null +++ b/foundations/hulypulse/scripts/TEST.html @@ -0,0 +1,147 @@ + + + + + WebSocket JSON Tester + + + + +

WebSocket JSON Tester

+ + + +
+ + + +

+ + + + + + + + + + + + + + + + + + + + + + +

Waiting for server response...
+ + + + + diff --git a/foundations/hulypulse/scripts/TEST00.sh b/foundations/hulypulse/scripts/TEST00.sh new file mode 100755 index 0000000000..b050c29890 --- /dev/null +++ b/foundations/hulypulse/scripts/TEST00.sh @@ -0,0 +1,151 @@ +#!/bin/bash + +clear +source ./pulse_lib.sh + +TOKEN=$(./token.sh claims.json) + +get "00000000-0000-0000-0000-000000000001/foo/bar1" +put "00000000-0000-0000-0000-000000000001/foo/bar1" "rediska" +get "00000000-0000-0000-0000-000000000001/foo/bar1" + +exit + +#echo ${TOKEN} +#exit +ZP="00000000-0000-0000-0000-000000000001/TESTS" + + + +put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" +put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" +put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" + +get "00000000-0000-0000-0000-000000000001/TESTS/" +sleep 2 +# get "00000000-0000-0000-0000-000000000001/TESTS/val2" +get "00000000-0000-0000-0000-000000000001/TESTS/" + + + +exit + + + + + + +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + +#exit + delete "00000000-0000-0000-0000-000000000001/TESTS" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" + +#exit + + put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" + get "00000000-0000-0000-0000-000000000001/TESTS/" + +#exit + + delete "0000000/TESTS" + delete ${ZP} + put ${ZP} "Value_1" "HULY-TTL: 2" + delete ${ZP} + +echo "--------- authorization_test ----------" +TOKEN="" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims_system.json) + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims_wrong_ws.json) + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims.json) + put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" + + + +echo "--------- if-match ----------" + + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" + get "00000000-0000-0000-0000-000000000001/TESTS" + get "00000000-0000-0000-0000-000000000001/TESTS/" + get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" + + +echo "--------- Deprecated symbols ----------" + + put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" + +echo "--------- if-match ----------" + + delete ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" + get ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" + get ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" + +echo "-- Expected OK: 201 Created (key was not exist)" + + put ${ZP} "enother text" "If-None-Match" "*" + + put ${ZP} "some text" + echo "-- Expected Error: 412 Precondition Failed (key was exist)" + put ${ZP} "enother text" "If-None-Match" "*" + +echo "================> UPDATE PUT If-Match" + + get ${ZP} + + echo "-- Expected OK: 204 No Content (right hash)" + put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + get ${ZP} + + echo "-- Expected OK: 204 No Content (hash still right)" + put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + + + + + + +put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" +echo "sleep 1 sec" +sleep 1 +get "00000000-0000-0000-0000-000000000001/TESTS" +echo "sleep 3 sec" +sleep 2 +get "00000000-0000-0000-0000-000000000001/TESTS" + +echo "--------- delete ----------" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" +get "00000000-0000-0000-0000-000000000001/TESTS" +delete "00000000-0000-0000-0000-000000000001/TESTS" +get "00000000-0000-0000-0000-000000000001/TESTS" + +echo "--------- prefix ----------" +put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" +get "00000000-0000-0000-0000-000000000001?prefix=TES" +sleep 1 +get "00000000-0000-0000-0000-000000000001?prefix=" + +exit diff --git a/foundations/hulypulse/scripts/TEST_HTTP_API.sh b/foundations/hulypulse/scripts/TEST_HTTP_API.sh new file mode 100755 index 0000000000..1b8b9816dd --- /dev/null +++ b/foundations/hulypulse/scripts/TEST_HTTP_API.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +clear +source ./pulse_lib.sh + +TOKEN=$(./token.sh claims.json) +#echo ${TOKEN} +#exit +ZP="00000000-0000-0000-0000-000000000001/TESTS" + + + +put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" +put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" +put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" + +get "00000000-0000-0000-0000-000000000001/TESTS/" +sleep 2 +# get "00000000-0000-0000-0000-000000000001/TESTS/val2" +get "00000000-0000-0000-0000-000000000001/TESTS/" + + + +#exit + + + + + + +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + +#exit + delete "00000000-0000-0000-0000-000000000001/TESTS" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" + +#exit + + put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" + get "00000000-0000-0000-0000-000000000001/TESTS/" + +#exit + + delete "0000000/TESTS" + delete ${ZP} + put ${ZP} "Value_1" "HULY-TTL: 2" + delete ${ZP} + +echo "--------- authorization_test ----------" +TOKEN="" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims_system.json) + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims_wrong_ws.json) + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims.json) + put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" + + + +echo "--------- if-match ----------" + + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" + get "00000000-0000-0000-0000-000000000001/TESTS" + get "00000000-0000-0000-0000-000000000001/TESTS/" + get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" + + +echo "--------- Deprecated symbols ----------" + + put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" + +echo "--------- if-match ----------" + + delete ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" + get ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" + get ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" + +echo "-- Expected OK: 201 Created (key was not exist)" + + put ${ZP} "enother text" "If-None-Match" "*" + + put ${ZP} "some text" + echo "-- Expected Error: 412 Precondition Failed (key was exist)" + put ${ZP} "enother text" "If-None-Match" "*" + +echo "================> UPDATE PUT If-Match" + + get ${ZP} + + echo "-- Expected OK: 204 No Content (right hash)" + put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + get ${ZP} + + echo "-- Expected OK: 204 No Content (hash still right)" + put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + + + + + + +put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" +echo "sleep 1 sec" +sleep 1 +get "00000000-0000-0000-0000-000000000001/TESTS" +echo "sleep 3 sec" +sleep 2 +get "00000000-0000-0000-0000-000000000001/TESTS" + +echo "--------- delete ----------" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" +get "00000000-0000-0000-0000-000000000001/TESTS" +delete "00000000-0000-0000-0000-000000000001/TESTS" +get "00000000-0000-0000-0000-000000000001/TESTS" + +echo "--------- prefix ----------" +put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" +get "00000000-0000-0000-0000-000000000001?prefix=TES" +sleep 1 +get "00000000-0000-0000-0000-000000000001?prefix=" + +exit diff --git a/foundations/hulypulse/scripts/TEST_HTTP_API_repo.sh b/foundations/hulypulse/scripts/TEST_HTTP_API_repo.sh new file mode 100755 index 0000000000..7ae9608387 --- /dev/null +++ b/foundations/hulypulse/scripts/TEST_HTTP_API_repo.sh @@ -0,0 +1,152 @@ +#!/bin/bash + +clear +source ./pulse_lib.sh + +TOKEN=$(./token.sh claims.json) + +put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" +get "00000000-0000-0000-0000-000000000001/TESTS/val1" +#list "00000000-0000-0000-0000-000000000001/TESTS/" +get "00000000-0000-0000-0000-000000000002/TESTS/val1" + +exit + +#echo ${TOKEN} + +ZP="00000000-0000-0000-0000-000000000001/TESTS" + + + +put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" +put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" +put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" + +get "00000000-0000-0000-0000-000000000001/TESTS/" +sleep 2 +# get "00000000-0000-0000-0000-000000000001/TESTS/val2" +get "00000000-0000-0000-0000-000000000001/TESTS/" + + + +#exit + + + + + + +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + +#exit + delete "00000000-0000-0000-0000-000000000001/TESTS" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" + +#exit + + put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" + get "00000000-0000-0000-0000-000000000001/TESTS/" + +#exit + + delete "0000000/TESTS" + delete ${ZP} + put ${ZP} "Value_1" "HULY-TTL: 2" + delete ${ZP} + +echo "--------- authorization_test ----------" +TOKEN="" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims_system.json) + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims_wrong_ws.json) + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims.json) + put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" + + + +echo "--------- if-match ----------" + + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" + get "00000000-0000-0000-0000-000000000001/TESTS" + get "00000000-0000-0000-0000-000000000001/TESTS/" + get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" + + +echo "--------- Deprecated symbols ----------" + + put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" + +echo "--------- if-match ----------" + + delete ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" + get ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" + get ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" + +echo "-- Expected OK: 201 Created (key was not exist)" + + put ${ZP} "enother text" "If-None-Match" "*" + + put ${ZP} "some text" + echo "-- Expected Error: 412 Precondition Failed (key was exist)" + put ${ZP} "enother text" "If-None-Match" "*" + +echo "================> UPDATE PUT If-Match" + + get ${ZP} + + echo "-- Expected OK: 204 No Content (right hash)" + put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + get ${ZP} + + echo "-- Expected OK: 204 No Content (hash still right)" + put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + + + + + + +put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" +echo "sleep 1 sec" +sleep 1 +get "00000000-0000-0000-0000-000000000001/TESTS" +echo "sleep 3 sec" +sleep 2 +get "00000000-0000-0000-0000-000000000001/TESTS" + +echo "--------- delete ----------" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" +get "00000000-0000-0000-0000-000000000001/TESTS" +delete "00000000-0000-0000-0000-000000000001/TESTS" +get "00000000-0000-0000-0000-000000000001/TESTS" + +echo "--------- prefix ----------" +put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" +get "00000000-0000-0000-0000-000000000001?prefix=TES" +sleep 1 +get "00000000-0000-0000-0000-000000000001?prefix=" + +exit diff --git a/foundations/hulypulse/scripts/TEST_WS_API.sh b/foundations/hulypulse/scripts/TEST_WS_API.sh new file mode 100755 index 0000000000..2d111e134c --- /dev/null +++ b/foundations/hulypulse/scripts/TEST_WS_API.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +clear +#source ./pulse_lib.sh + +websocat ws://127.0.0.1:8095/ws/testworkspace + +exit + + +let ws = new WebSocket("ws://localhost:8095/ws/testworkspace"); +ws.onmessage = e => console.log("Message from server:", e.data); +ws.onopen = () => ws.send("Hello from browser!"); + + + + + + + + + + + + + + + + + + + +TOKEN=$(./token.sh claims.json) +ZP="00000000-0000-0000-0000-000000000001/TESTS" +# /AnyKey" + +# put ${ZP} "one text" + +# put "00000000-0000-0000-0000-000000000001/TESTS" "text 1" "If-None-Match: *" "Blooooooooo: blya" + +#exit + +#put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" +#echo "sleep 1 sec" +#sleep 1 +#get "00000000-0000-0000-0000-000000000001/TESTS" +#echo "sleep 3 sec" +#sleep 2 +#get "00000000-0000-0000-0000-000000000001/TESTS" + +put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" +get "00000000-0000-0000-0000-000000000001?prefix=TES" +sleep 1 +get "00000000-0000-0000-0000-000000000001?prefix=" + +exit diff --git a/foundations/hulypulse/scripts/TEST_lleo.html b/foundations/hulypulse/scripts/TEST_lleo.html new file mode 100644 index 0000000000..3a9608a5e1 --- /dev/null +++ b/foundations/hulypulse/scripts/TEST_lleo.html @@ -0,0 +1,129 @@ + + + + + WebSocket JSON Tester + + + + +

WebSocket JSON Tester

+ + + +
+ + + +

+ + + + + + + + + +

Waiting for server response...
+ + + + + diff --git a/foundations/hulypulse/scripts/TEST_no_auth.html b/foundations/hulypulse/scripts/TEST_no_auth.html new file mode 100644 index 0000000000..20e2e14c30 --- /dev/null +++ b/foundations/hulypulse/scripts/TEST_no_auth.html @@ -0,0 +1,149 @@ + + + + + WebSocket JSON Tester + + + + +

WebSocket JSON Tester

+ + + +
+ + + +

+ + + + + + + + + + + + + + + + + + + + + + +

Waiting for server response...
+ + + + + \ No newline at end of file diff --git a/foundations/hulypulse/scripts/claims.json b/foundations/hulypulse/scripts/claims.json new file mode 100644 index 0000000000..af4a64c94b --- /dev/null +++ b/foundations/hulypulse/scripts/claims.json @@ -0,0 +1,7 @@ +{ + "extra": { + "service": "account" + }, + "account": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "workspace": "00000000-0000-0000-0000-000000000001" +} diff --git a/foundations/hulypulse/scripts/claims2.json b/foundations/hulypulse/scripts/claims2.json new file mode 100644 index 0000000000..ed17b895b8 --- /dev/null +++ b/foundations/hulypulse/scripts/claims2.json @@ -0,0 +1,7 @@ +{ + "extra": { + "service": "account" + }, + "account": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "workspace": "00000000-0000-0000-0000-000000000002" +} diff --git a/foundations/hulypulse/scripts/claims_exp.json b/foundations/hulypulse/scripts/claims_exp.json new file mode 100644 index 0000000000..dcc90fe4b8 --- /dev/null +++ b/foundations/hulypulse/scripts/claims_exp.json @@ -0,0 +1,8 @@ +{ + "extra": { + "service": "account" + }, + "account": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "workspace": "00000000-0000-0000-0000-000000000001", + "exp": 1924236800 +} diff --git a/foundations/hulypulse/scripts/claims_system.json b/foundations/hulypulse/scripts/claims_system.json new file mode 100644 index 0000000000..a6f400009e --- /dev/null +++ b/foundations/hulypulse/scripts/claims_system.json @@ -0,0 +1,7 @@ +{ + "extra": { + "service": "account" + }, + "account": "1749089e-22e6-48de-af4e-165e18fbd2f9", + "workspace": "00000000-0000-0000-0000-000000000001" +} diff --git a/foundations/hulypulse/scripts/claims_wrong_ws.json b/foundations/hulypulse/scripts/claims_wrong_ws.json new file mode 100644 index 0000000000..8bd456b086 --- /dev/null +++ b/foundations/hulypulse/scripts/claims_wrong_ws.json @@ -0,0 +1,7 @@ +{ + "extra": { + "service": "account" + }, + "account": "lleo", + "workspace": "00000000-0000-0000-0000-000000000002" +} diff --git a/foundations/hulypulse/scripts/lleo_TEST_HTTP_API.sh b/foundations/hulypulse/scripts/lleo_TEST_HTTP_API.sh new file mode 100755 index 0000000000..a30faf5608 --- /dev/null +++ b/foundations/hulypulse/scripts/lleo_TEST_HTTP_API.sh @@ -0,0 +1,149 @@ +#!/bin/bash + +clear +source ./pulse_lib_lleo.sh + +#TOKEN=$(./token.sh claims.json) +#echo ${TOKEN} + + + +put "dnevnik/onlline/admin" "oki" "HULY-TTL: 3" + + + +exit +ZP="00000000-0000-0000-0000-000000000001/TESTS" + +put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" +put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 3" + +get "00000000-0000-0000-0000-000000000001/TESTS/" +sleep 4 +# get "00000000-0000-0000-0000-000000000001/TESTS/val2" +get "00000000-0000-0000-0000-000000000001/TESTS/" + + + +exit + + + + + + +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + +#exit + delete "00000000-0000-0000-0000-000000000001/TESTS" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" + +#exit + + put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" + get "00000000-0000-0000-0000-000000000001/TESTS/" + +#exit + + delete "0000000/TESTS" + delete ${ZP} + put ${ZP} "Value_1" "HULY-TTL: 2" + delete ${ZP} + +echo "--------- authorization_test ----------" +TOKEN="" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims_system.json) + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims_wrong_ws.json) + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims.json) + put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" + + + +echo "--------- if-match ----------" + + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" + get "00000000-0000-0000-0000-000000000001/TESTS" + get "00000000-0000-0000-0000-000000000001/TESTS/" + get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" + + +echo "--------- Deprecated symbols ----------" + + put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" + +echo "--------- if-match ----------" + + delete ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" + get ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" + get ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" + +echo "-- Expected OK: 201 Created (key was not exist)" + + put ${ZP} "enother text" "If-None-Match" "*" + + put ${ZP} "some text" + echo "-- Expected Error: 412 Precondition Failed (key was exist)" + put ${ZP} "enother text" "If-None-Match" "*" + +echo "================> UPDATE PUT If-Match" + + get ${ZP} + + echo "-- Expected OK: 204 No Content (right hash)" + put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + get ${ZP} + + echo "-- Expected OK: 204 No Content (hash still right)" + put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + + + + + + +put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" +echo "sleep 1 sec" +sleep 1 +get "00000000-0000-0000-0000-000000000001/TESTS" +echo "sleep 3 sec" +sleep 2 +get "00000000-0000-0000-0000-000000000001/TESTS" + +echo "--------- delete ----------" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" +get "00000000-0000-0000-0000-000000000001/TESTS" +delete "00000000-0000-0000-0000-000000000001/TESTS" +get "00000000-0000-0000-0000-000000000001/TESTS" + +echo "--------- prefix ----------" +put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" +get "00000000-0000-0000-0000-000000000001?prefix=TES" +sleep 1 +get "00000000-0000-0000-0000-000000000001?prefix=" + +exit diff --git a/foundations/hulypulse/scripts/pulse_lib.sh b/foundations/hulypulse/scripts/pulse_lib.sh new file mode 100755 index 0000000000..19f565c8f8 --- /dev/null +++ b/foundations/hulypulse/scripts/pulse_lib.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +clear + +URL="http://localhost:8099/api" + +R='\033[0;31m' # Color red +G='\033[0;32m' # Color green +W='\033[0;33m' # Color ? +S='\033[0;34m' # Color Blue +F='\033[0;35m' # Color Fiolet +L='\033[0;36m' # Color LightBlue +N='\033[0m' # No Color +GRAY='\033[90m' # bright black + +api() { + local tmpfile + tmpfile=$1 + local status + status=$(head -n 1 "$tmpfile") + local status_code + status_code=$(echo "$status" | awk '{print $2}') + local etag + etag=$(grep -i "^ETag:" "${tmpfile}") + local body + body=$(awk 'found { print; next } NF == 0 { found = 1 }' "$tmpfile") + case "$status_code" in + 2*) echo -en "${G}${status}${N}" ;; + 3*) echo -en "${F}${status}${N}" ;; + 4*) echo -en "${R}${status}${N}" ;; + 5*) echo -en "${R}${status}${N}" ;; + *) echo -en "${GRAY}${status}${N}" ;; + esac + if [ -n "$etag" ]; then echo -n -e " ${F}${etag}${N}" ; fi + + body=$(echo "$body" | sed 's/{/\\n{/g') + + if [ -n "$body" ]; then echo -e "\n ${GRAY}[${body}]${N}" ; else echo -e " ${L}(no body)${N}" ; fi + rm -f "$tmpfile" +} + +get() { + echo -n -e "📥 ${L}GET ${W}$1${N} > " + local tmpfile + tmpfile=$(mktemp) + curl -i -s -X GET "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" + api ${tmpfile} +} + +put() { # If-None-Match If-Match + local match + local match_prn +# if [ -n "$3" ]; then match=(-H "$3: $4") ; else match=() ; fi +# if [ -n "$3" ]; then match_prn=" ${F}$3:$4${N}" ; else match_prn="" ; fi +# echo -n -e "📥 ${L}PUT ${W}$1${N}${match_prn} > " + + if [ -n "$3" ]; then match1=(-H "$3") ; else match1=() ; fi + if [ -n "$3" ]; then match1_prn=" ${F}$3${N}" ; else match1_prn="" ; fi + if [ -n "$4" ]; then match2=(-H "$4") ; else match2=() ; fi + if [ -n "$4" ]; then match2_prn=" ${F}$4${N}" ; else match2_prn="" ; fi + echo -n -e "📥 ${L}PUT ${W}$1${N}${match1_prn}${match2_prn} > " + + local tmpfile + tmpfile=$(mktemp) +# curl -v -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" + curl -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" + api ${tmpfile} +} + +delete() { + echo -n -e "📥 ${L}DELETE ${W}$1${N} > " + local tmpfile + tmpfile=$(mktemp) + curl -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" +# curl -v -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" + api ${tmpfile} +} diff --git a/foundations/hulypulse/scripts/pulse_lib_huly.sh b/foundations/hulypulse/scripts/pulse_lib_huly.sh new file mode 100755 index 0000000000..a500cf371f --- /dev/null +++ b/foundations/hulypulse/scripts/pulse_lib_huly.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +clear + +# URL="http://localhost:8095/api" +URL="http://huly.local:8099/api" + +R='\033[0;31m' # Color red +G='\033[0;32m' # Color green +W='\033[0;33m' # Color ? +S='\033[0;34m' # Color Blue +F='\033[0;35m' # Color Fiolet +L='\033[0;36m' # Color LightBlue +N='\033[0m' # No Color +GRAY='\033[90m' # bright black + +api() { + local tmpfile + tmpfile=$1 + local status + status=$(head -n 1 "$tmpfile") + local status_code + status_code=$(echo "$status" | awk '{print $2}') + local etag + etag=$(grep -i "^ETag:" "${tmpfile}") + local body + body=$(awk 'found { print; next } NF == 0 { found = 1 }' "$tmpfile") + case "$status_code" in + 2*) echo -en "${G}${status}${N}" ;; + 3*) echo -en "${F}${status}${N}" ;; + 4*) echo -en "${R}${status}${N}" ;; + 5*) echo -en "${R}${status}${N}" ;; + *) echo -en "${GRAY}${status}${N}" ;; + esac + if [ -n "$etag" ]; then echo -n -e " ${F}${etag}${N}" ; fi + + body=$(echo "$body" | sed 's/{/\\n{/g') + + if [ -n "$body" ]; then echo -e "\n ${GRAY}[${body}]${N}" ; else echo -e " ${L}(no body)${N}" ; fi + rm -f "$tmpfile" +} + +get() { + echo -n -e "📥 ${L}GET ${W}$1${N} > " + local tmpfile + tmpfile=$(mktemp) + curl -i -s -X GET "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" + api ${tmpfile} +} + +put() { # If-None-Match If-Match + local match + local match_prn +# if [ -n "$3" ]; then match=(-H "$3: $4") ; else match=() ; fi +# if [ -n "$3" ]; then match_prn=" ${F}$3:$4${N}" ; else match_prn="" ; fi +# echo -n -e "📥 ${L}PUT ${W}$1${N}${match_prn} > " + + if [ -n "$3" ]; then match1=(-H "$3") ; else match1=() ; fi + if [ -n "$3" ]; then match1_prn=" ${F}$3${N}" ; else match1_prn="" ; fi + if [ -n "$4" ]; then match2=(-H "$4") ; else match2=() ; fi + if [ -n "$4" ]; then match2_prn=" ${F}$4${N}" ; else match2_prn="" ; fi + echo -n -e "📥 ${L}PUT ${W}$1${N}${match1_prn}${match2_prn} > " + + local tmpfile + tmpfile=$(mktemp) +# curl -v -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" + curl -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" + api ${tmpfile} +} + +delete() { + echo -n -e "📥 ${L}DELETE ${W}$1${N} > " + local tmpfile + tmpfile=$(mktemp) + curl -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" +# curl -v -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" + api ${tmpfile} +} diff --git a/foundations/hulypulse/scripts/pulse_lib_lleo.sh b/foundations/hulypulse/scripts/pulse_lib_lleo.sh new file mode 100755 index 0000000000..4cf8832fca --- /dev/null +++ b/foundations/hulypulse/scripts/pulse_lib_lleo.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +clear + +# URL="http://localhost:8095/api" +URL="https://hulypulse_mem.lleo.me/api" + +R='\033[0;31m' # Color red +G='\033[0;32m' # Color green +W='\033[0;33m' # Color ? +S='\033[0;34m' # Color Blue +F='\033[0;35m' # Color Fiolet +L='\033[0;36m' # Color LightBlue +N='\033[0m' # No Color +GRAY='\033[90m' # bright black + +api() { + local tmpfile + tmpfile=$1 + local status + status=$(head -n 1 "$tmpfile") + local status_code + status_code=$(echo "$status" | awk '{print $2}') + local etag + etag=$(grep -i "^ETag:" "${tmpfile}") + local body + body=$(awk 'found { print; next } NF == 0 { found = 1 }' "$tmpfile") + case "$status_code" in + 2*) echo -en "${G}${status}${N}" ;; + 3*) echo -en "${F}${status}${N}" ;; + 4*) echo -en "${R}${status}${N}" ;; + 5*) echo -en "${R}${status}${N}" ;; + *) echo -en "${GRAY}${status}${N}" ;; + esac + if [ -n "$etag" ]; then echo -n -e " ${F}${etag}${N}" ; fi + + body=$(echo "$body" | sed 's/{/\\n{/g') + + if [ -n "$body" ]; then echo -e "\n ${GRAY}[${body}]${N}" ; else echo -e " ${L}(no body)${N}" ; fi + rm -f "$tmpfile" +} + +get() { + echo -n -e "📥 ${L}GET ${W}$1${N} > " + local tmpfile + tmpfile=$(mktemp) + curl -i -s -X GET "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" + api ${tmpfile} +} + +put() { # If-None-Match If-Match + local match + local match_prn +# if [ -n "$3" ]; then match=(-H "$3: $4") ; else match=() ; fi +# if [ -n "$3" ]; then match_prn=" ${F}$3:$4${N}" ; else match_prn="" ; fi +# echo -n -e "📥 ${L}PUT ${W}$1${N}${match_prn} > " + + if [ -n "$3" ]; then match1=(-H "$3") ; else match1=() ; fi + if [ -n "$3" ]; then match1_prn=" ${F}$3${N}" ; else match1_prn="" ; fi + if [ -n "$4" ]; then match2=(-H "$4") ; else match2=() ; fi + if [ -n "$4" ]; then match2_prn=" ${F}$4${N}" ; else match2_prn="" ; fi + echo -n -e "📥 ${L}PUT ${W}$1${N}${match1_prn}${match2_prn} > " + + local tmpfile + tmpfile=$(mktemp) +# curl -v -i -s -X PUT "$URL/$1" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" +# curl -v -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" + curl -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" + api ${tmpfile} +} + +delete() { + echo -n -e "📥 ${L}DELETE ${W}$1${N} > " + local tmpfile + tmpfile=$(mktemp) + curl -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" +# curl -v -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" + api ${tmpfile} +} diff --git a/foundations/hulypulse/scripts/test_pulse.sh b/foundations/hulypulse/scripts/test_pulse.sh new file mode 100755 index 0000000000..808173ffe9 --- /dev/null +++ b/foundations/hulypulse/scripts/test_pulse.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +clear +source ./pulse_lib.sh + +TOKEN=$(./token.sh claims.json) +ZP="00000000-0000-0000-0000-000000000001/TESTS/AnyKey" + +echo "================> LIST" + put "00000000-0000-0000-0000-000000000001/Huome2/MyKey1" "value1" + put "00000000-0000-0000-0000-000000000001/Huome2/MyKey2" "value2" + get "00000000-0000-0000-0000-000000000001/Huome2" + delete "00000000-0000-0000-0000-000000000001/Huome2/MyKey1" + delete "00000000-0000-0000-0000-000000000001/Huome2/MyKey2" + +echo "================> WRONG UUID" + get "WrongUUID/TESTS/AnyKey" + +echo "================> INSERT If-None-Match" + + echo "-- Expected Error: 400 Bad Request (If-None-Match may be only *)" + put ${ZP} "enother text" "If-None-Match" "552e21cd4cd9918678e3c1a0df491bc3" + + delete ${ZP} + + echo "-- Expected OK: 201 Created (key was not exist)" + put ${ZP} "enother text" "If-None-Match" "*" + + put ${ZP} "some text" + echo "-- Expected Error: 412 Precondition Failed (key was exist)" + put ${ZP} "enother text" "If-None-Match" "*" + +echo "================> UPDATE PUT If-Match" + + get ${ZP} + + echo "-- Expected OK: 204 No Content (right hash)" + put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + get ${ZP} + + echo "-- Expected OK: 204 No Content (hash still right)" + put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + get ${ZP} + + echo "-- Expected OK: 204 No Content (any hash)" + put ${ZP} "enother version2" "If-Match" "*" + get ${ZP} + + echo "-- Expected Error: 412 Precondition Failed (wrong hash)" + put ${ZP} "enother version3" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + + delete ${ZP} + + echo "-- Expected Error: 412 Precondition Failed (any hash not found)" + put ${ZP} "enother version2" "If-Match" "*" + +echo "================> UPSERT (Expected OK)" + put ${ZP} "my value" + get ${ZP} + put ${ZP} "my new value" + get ${ZP} + +exit diff --git a/foundations/hulypulse/scripts/test_pulse_system.sh b/foundations/hulypulse/scripts/test_pulse_system.sh new file mode 100755 index 0000000000..be2c78fa0d --- /dev/null +++ b/foundations/hulypulse/scripts/test_pulse_system.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +clear +source ./pulse_lib.sh + +TOKEN_OK=$(./token.sh claims.json) +TOKEN_SYSTEM=$(./token.sh claims_system.json) +TOKEN_WRONG=$(./token.sh claims_wrong_ws.json) +ZP="00000000-0000-0000-0000-000000000001/TESTS/JWT_tests" + +echo "================> SYSTEM change - OK" + TOKEN=${TOKEN_SYSTEM} + # delete ${ZP} + put ${ZP} "system value" + +echo "================> USER read/change - OK" + TOKEN=${TOKEN_OK} + get ${ZP} + put ${ZP} "user value" + +echo "================> WRONG USER read/change - ERROR" + TOKEN=${TOKEN_WRONG} + get ${ZP} + put ${ZP} "wrong user value" + +echo "================> SYSTEM read/change - OK" + TOKEN=${TOKEN_SYSTEM} + get ${ZP} + put ${ZP} "system value 2" + +echo "================> USER read - OK" + TOKEN=${TOKEN_OK} + get ${ZP} + +exit diff --git a/foundations/hulypulse/scripts/token.sh b/foundations/hulypulse/scripts/token.sh new file mode 100755 index 0000000000..78ae2ef971 --- /dev/null +++ b/foundations/hulypulse/scripts/token.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +CONFIG_PATH="../src/config/default.toml" +SECRET=$(grep '^token_secret' "$CONFIG_PATH" | sed -E 's/.*=\s*"(.*)"/\1/') # " + +if [ -z "$SECRET" ]; then + echo "❌No token_secret in $CONFIG_PATH" + exit 1 +fi + +claims=$1 # "claims.json" + +#TOKEN=$(echo -n "${SECRET}" | jwt -alg HS256 -key - -sign claims.json) +TOKEN=$(echo -n "${SECRET}" | jwt -alg HS256 -key - -sign ${claims}) + +echo "$TOKEN" diff --git a/foundations/hulypulse/scripts/typing-test.sh b/foundations/hulypulse/scripts/typing-test.sh new file mode 100755 index 0000000000..4c7990b504 --- /dev/null +++ b/foundations/hulypulse/scripts/typing-test.sh @@ -0,0 +1,151 @@ +#!/bin/bash + +clear +source ./pulse_lib_huly.sh + +#TOKEN=$(./token.sh claims.json) +TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHRyYSI6e30sImFjY291bnQiOiI1NjBlNDRiYS1jM2ZhLTRmMzUtYjQxYi00NWMzY2FhYWZiZTAiLCJ3b3Jrc3BhY2UiOiI4NTk5ZWViZS0xZDEwLTRhNDYtYTIxZS04OWNkMzI4YjRmZmEifQ.rTmKG5ulwTONs6KPfmBOLnY6BaXfwP1kma_Pvay-pz8" +echo ${TOKEN} + +#exit +#ZP="00000000-0000-0000-0000-000000000001/TESTS" + +#put "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/chunter:space:General/68874fd619a81293751d001e" "{\"personId\":\"68874fd619a81293751d001e\",\"objectId\":\"chunter:space:General\"}" "HULY-TTL: 120" + +put "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/68e259323d9a9ae45c7dd0ea/68874fd619a81293751d001e" "{\"personId\":\"68874fd619a81293751d001e\",\"objectId\":\"68e259323d9a9ae45c7dd0ea\"}" "HULY-TTL: 15" +put "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/68e259323d9a9ae45c7dd0ea/68e2585a62753bede49ee803" "{\"personId\":\"68e2585a62753bede49ee803\",\"objectId\":\"68e259323d9a9ae45c7dd0ea\"}" "HULY-TTL: 15" +# "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/68e259323d9a9ae45c7dd0ea/" + +#put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" +#put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 120" +#put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" +#get "00000000-0000-0000-0000-000000000001/TESTS/" +#sleep 2 +# get "00000000-0000-0000-0000-000000000001/TESTS/val2" +#get "00000000-0000-0000-0000-000000000001/TESTS/" + + + +exit + +http://huly.local:8099/status + + + + + + +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + +#exit + delete "00000000-0000-0000-0000-000000000001/TESTS" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value" + delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" + +#exit + + put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" + get "00000000-0000-0000-0000-000000000001/TESTS/" + +#exit + + delete "0000000/TESTS" + delete ${ZP} + put ${ZP} "Value_1" "HULY-TTL: 2" + delete ${ZP} + +echo "--------- authorization_test ----------" +TOKEN="" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims_system.json) + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims_wrong_ws.json) + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" +TOKEN=$(./token.sh claims.json) + put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" + + + +echo "--------- if-match ----------" + + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" + get "00000000-0000-0000-0000-000000000001/TESTS" + get "00000000-0000-0000-0000-000000000001/TESTS/" + get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" + + +echo "--------- Deprecated symbols ----------" + + put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" + put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" + +echo "--------- if-match ----------" + + delete ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" + get ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" + get ${ZP} + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" + put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" + +echo "-- Expected OK: 201 Created (key was not exist)" + + put ${ZP} "enother text" "If-None-Match" "*" + + put ${ZP} "some text" + echo "-- Expected Error: 412 Precondition Failed (key was exist)" + put ${ZP} "enother text" "If-None-Match" "*" + +echo "================> UPDATE PUT If-Match" + + get ${ZP} + + echo "-- Expected OK: 204 No Content (right hash)" + put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + get ${ZP} + + echo "-- Expected OK: 204 No Content (hash still right)" + put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" + + + + + + +put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" +echo "sleep 1 sec" +sleep 1 +get "00000000-0000-0000-0000-000000000001/TESTS" +echo "sleep 3 sec" +sleep 2 +get "00000000-0000-0000-0000-000000000001/TESTS" + +echo "--------- delete ----------" +put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" +get "00000000-0000-0000-0000-000000000001/TESTS" +delete "00000000-0000-0000-0000-000000000001/TESTS" +get "00000000-0000-0000-0000-000000000001/TESTS" + +echo "--------- prefix ----------" +put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" +put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" +get "00000000-0000-0000-0000-000000000001?prefix=TES" +sleep 1 +get "00000000-0000-0000-0000-000000000001?prefix=" + +exit diff --git a/foundations/hulypulse/src/GOT.sh b/foundations/hulypulse/src/GOT.sh new file mode 100755 index 0000000000..9497816776 --- /dev/null +++ b/foundations/hulypulse/src/GOT.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +clear + +redis-cli set lleo value +redis-cli del lleo +redis-cli set ttlkey 1 EX 2 +# подожди ~2 сек → должно прийти expired diff --git a/foundations/hulypulse/src/config.rs b/foundations/hulypulse/src/config.rs new file mode 100644 index 0000000000..ab204576f0 --- /dev/null +++ b/foundations/hulypulse/src/config.rs @@ -0,0 +1,90 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use std::{path::Path, sync::LazyLock}; + +use secrecy::SecretString; +use serde::Deserialize; +use serde_with::formats::CommaSeparator; +use serde_with::{StringWithSeparator, serde_as}; +use url::Url; + +use config::FileFormat; + +#[derive(Deserialize, Debug, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum RedisMode { + Sentinel, + Direct, +} + +#[derive(Deserialize, Debug, PartialEq, strum::Display)] +#[serde(rename_all = "lowercase")] +pub enum BackendType { + Memory, + Redis, +} + +#[serde_as] +#[derive(Deserialize, Debug)] +pub struct Config { + pub bind_port: u16, + pub bind_host: String, + + pub token_secret: SecretString, + + #[serde_as(as = "StringWithSeparator::")] + pub redis_urls: Vec, + pub redis_password: String, + pub redis_mode: RedisMode, + pub redis_service: String, + + pub max_ttl: usize, + pub max_size: Option, + + pub backend: BackendType, + pub no_authorization: bool, + + pub heartbeat_timeout: u64, + pub ping_timeout: u64, + + pub policy_file: Option, +} + +pub static CONFIG: LazyLock = LazyLock::new(|| { + const DEFAULTS: &str = std::include_str!("config/default.toml"); + + let mut builder = + config::Config::builder().add_source(config::File::from_str(DEFAULTS, FileFormat::Toml)); + + let path = Path::new("etc/config.toml"); + + if path.exists() { + builder = builder.add_source(config::File::with_name(path.as_os_str().to_str().unwrap())); + } + + let settings = builder + .add_source(config::Environment::with_prefix("HULY")) + .build() + .and_then(|c| c.try_deserialize::()); + + match settings { + Ok(settings) => settings, + Err(error) => { + eprintln!("configuration error: {}", error); + std::process::exit(1); + } + } +}); diff --git a/foundations/hulypulse/src/config/default.toml b/foundations/hulypulse/src/config/default.toml new file mode 100644 index 0000000000..5e8d1228ec --- /dev/null +++ b/foundations/hulypulse/src/config/default.toml @@ -0,0 +1,21 @@ +bind_port = 8099 +bind_host = "0.0.0.0" + +token_secret = "secret" + +redis_urls = "redis://huly.local:6379" +redis_password = "" +redis_mode = "direct" +redis_service = "mymaster" + +max_ttl = 3600 +backend = "redis" +no_authorization = false + +heartbeat_timeout = 90 +ping_timeout = 30 + +# optional settings +# max_size = 100 + +# permit_file = "/home/user/hulipulse/permit.rego" diff --git a/foundations/hulypulse/src/db.rs b/foundations/hulypulse/src/db.rs new file mode 100644 index 0000000000..aadac7c1ab --- /dev/null +++ b/foundations/hulypulse/src/db.rs @@ -0,0 +1,128 @@ +use std::sync::Arc; + +use crate::{ + hub_service::{HubState, RedisEvent, RedisEventAction, broadcast_event}, + memory::{MemoryBackend, memory_delete, memory_info, memory_list, memory_read, memory_save}, + redis::{ + RedisArray, SaveMode, Ttl, redis_delete, redis_info, redis_list, redis_read, redis_save, + }, +}; +use ::redis::aio::MultiplexedConnection; +use tokio::sync::RwLock; + +#[derive(Clone)] +pub struct Db { + inner: DbInner, + hub: Arc>, +} + +#[derive(Clone)] +enum DbInner { + Memory(MemoryBackend), + Redis(MultiplexedConnection), +} + +impl Db { + pub fn new_memory(m: MemoryBackend, hub: Arc>) -> Self { + Self { + inner: DbInner::Memory(m), + hub, + } + } + pub fn new_redis(c: MultiplexedConnection, hub: Arc>) -> Self { + Self { + inner: DbInner::Redis(c), + hub, + } + } + + pub async fn info(&self) -> redis::RedisResult { + // String { + // let res = + match &self.inner { + DbInner::Memory(m) => memory_info(m).await, + DbInner::Redis(conn) => { + let mut c = conn.clone(); + redis_info(&mut c).await + } + } + // }; + // res.unwrap_or_else(|_| "error".to_string()) + } + + pub async fn list(&self, key: &str) -> redis::RedisResult> { + match &self.inner { + DbInner::Memory(m) => memory_list(m, key).await, + DbInner::Redis(conn) => { + let mut c = conn.clone(); + redis_list(&mut c, key).await + } + } + } + + pub async fn read(&self, key: &str) -> redis::RedisResult> { + match &self.inner { + DbInner::Memory(m) => memory_read(m, key).await, + DbInner::Redis(conn) => { + let mut c = conn.clone(); + redis_read(&mut c, key).await + } + } + } + + pub async fn save>( + &self, + key: &str, + value: V, + ttl: Option, + mode: Option, + ) -> redis::RedisResult<()> { + match &self.inner { + DbInner::Memory(m) => { + memory_save(m, key, value.as_ref(), ttl, mode).await?; + // Send events + let value_str = std::str::from_utf8(value.as_ref()) + .ok() + .map(|s| s.to_string()); + broadcast_event( + &self.hub, + RedisEvent { + message: RedisEventAction::Set, + key: key.to_string(), + }, + value_str, + ) + .await; + Ok(()) + } + DbInner::Redis(conn) => { + let mut c = conn.clone(); + redis_save(&mut c, key, value.as_ref(), ttl, mode).await + } + } + } + + pub async fn delete(&self, key: &str, mode: Option) -> redis::RedisResult { + match &self.inner { + DbInner::Memory(m) => { + let deleted = memory_delete(m, key, mode).await?; + if deleted { + broadcast_event( + &self.hub, + RedisEvent { + message: RedisEventAction::Del, + key: key.to_string(), + }, + None, + ) + .await; + } + Ok(deleted) + } + DbInner::Redis(conn) => { + let mut c = conn.clone(); + redis_delete(&mut c, key, mode).await + } + } + } +} diff --git a/foundations/hulypulse/src/handlers_http.rs b/foundations/hulypulse/src/handlers_http.rs new file mode 100644 index 0000000000..4de72250e4 --- /dev/null +++ b/foundations/hulypulse/src/handlers_http.rs @@ -0,0 +1,267 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use serde::Deserialize; +use std::str::FromStr; +use tracing::*; + +use actix_web::{ + Error, HttpRequest, HttpResponse, + error::ParseError, + http::header::{self, HeaderName, HeaderValue, IfMatch, IfNoneMatch, TryIntoHeaderValue}, + web, +}; + +use crate::{ + config::CONFIG, + db::Db, + redis::{SaveMode, Ttl}, + workspace_owner::test_rego_http, +}; + +pub fn map_redis_error(err: impl std::fmt::Display) -> Error { + let msg = err.to_string(); + + if let Some(detail) = msg.split(" - ExtensionError: ").nth(1) { + if let Some((code, text)) = detail.split_once(": ") { + let text = format!("{} {}", code, text); + return match code { + "400" => actix_web::error::ErrorBadRequest(text), + "404" => actix_web::error::ErrorNotFound(text), + "412" => actix_web::error::ErrorPreconditionFailed(text), + "500" => actix_web::error::ErrorInternalServerError(text), + _ => actix_web::error::ErrorInternalServerError("unexpected error"), + }; + } + } + actix_web::error::ErrorInternalServerError("internal error") +} + +#[derive(Deserialize, Debug)] +pub struct PathParams { + key: String, + workspace: String, +} + +pub struct TtlSecsHeader(Option); +pub struct TtlExpiresAtHeader(Option); + +/// list +pub async fn list( + req: HttpRequest, + path: web::Path, + db: web::Data, +) -> Result { + let params = path.into_inner(); + let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); + trace!(key, "list request"); + + if !CONFIG.no_authorization && !test_rego_http(req, "List", &key) { + return Err(actix_web::error::ErrorForbidden("forbidden")); + } + + let entries = db.list(&key).await.map_err(map_redis_error)?; + Ok(HttpResponse::Ok().json(entries)) +} + +/// get +pub async fn get( + req: HttpRequest, + path: web::Path, + db: web::Data, +) -> Result { + let params = path.into_inner(); + let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); + trace!(key, "get request"); + + if !CONFIG.no_authorization && !test_rego_http(req, "Get", &key) { + return Err(actix_web::error::ErrorForbidden("forbidden")); + } + + let entry_opt = db.read(&key).await.map_err(map_redis_error)?; + let resp = match entry_opt { + Some(entry) => HttpResponse::Ok() + .insert_header((header::ETAG, entry.etag.clone())) + .json(entry), + None => HttpResponse::NotFound().body("empty"), + }; + Ok(resp) +} + +/// put +pub async fn put( + req: HttpRequest, + path: web::Path, + body: web::Bytes, + db: web::Data, + (secs, expires_at): ( + Result, ParseError>, + Result, ParseError>, + ), + (if_match, if_none_match): ( + web::Header, + web::Header, + ), +) -> Result { + let params = path.into_inner(); + let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); + trace!(key, "put request"); + + if !CONFIG.no_authorization && !test_rego_http(req, "Put", &key) { + return Err(actix_web::error::ErrorForbidden("forbidden")); + } + + // TTL logic + let ttl = match (secs?.into_inner().0, expires_at?.into_inner().0) { + (None, None) => None, + (Some(secs), None) => Some(Ttl::Sec(secs)), + (None, Some(timestamp)) => Some(Ttl::At(timestamp)), + _ => { + return Err(actix_web::error::ErrorBadRequest("Multiple ttl specified")); + } + }; + + // MODE logic + let mode = match (if_match.into_inner(), if_none_match.into_inner()) { + (IfMatch::Items(items), IfNoneMatch::Items(nitems)) + if items.is_empty() && nitems.is_empty() => + { + SaveMode::Upsert + } + (IfMatch::Any, IfNoneMatch::Items(nitems)) if nitems.is_empty() => SaveMode::Update, + (IfMatch::Items(etags), IfNoneMatch::Items(nitems)) + if etags.len() == 1 && nitems.is_empty() => + { + SaveMode::Equal(etags[0].tag().to_string()) + } + (IfMatch::Items(items), IfNoneMatch::Any) if items.is_empty() => SaveMode::Insert, + _ => { + return Err(actix_web::error::ErrorBadRequest( + "Unsupported combination of If-Match and If-None-Match", + )); + } + }; + + db.save(&key, &body[..], ttl, Some(mode)) + .await + .map_err(map_redis_error)?; + Ok(HttpResponse::Ok().body("DONE")) +} + +/// delete +pub async fn delete( + req: HttpRequest, + path: web::Path, + db: web::Data, + if_match: web::Header, +) -> Result { + let params = path.into_inner(); + let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); + trace!(key, "delete request"); + + if !CONFIG.no_authorization && !test_rego_http(req, "Delete", &key) { + return Err(actix_web::error::ErrorForbidden("forbidden")); + } + + // MODE logic + let mode = match if_match.into_inner() { + IfMatch::Any => SaveMode::Update, + IfMatch::Items(etags) => { + if etags.len() == 1 { + SaveMode::Equal(etags[0].tag().to_string()) + } else if etags.is_empty() { + SaveMode::Upsert + } else { + return Err(actix_web::error::ErrorBadRequest( + "Multiple If-Match are not supported", + )); + } + } + }; + + let deleted = db.delete(&key, Some(mode)).await.map_err(map_redis_error)?; + let response = match deleted { + true => HttpResponse::NoContent().finish(), + false => HttpResponse::NotFound().body("not found"), + }; + + Ok(response) +} + +impl TryIntoHeaderValue for TtlSecsHeader { + type Error = std::convert::Infallible; + + fn try_into_value(self) -> Result { + Ok(self + .0 + .map(HeaderValue::from) + .unwrap_or(HeaderValue::from_static(""))) + } +} + +impl header::Header for TtlSecsHeader { + fn name() -> HeaderName { + HeaderName::from_static("huly-ttl") + } + + fn parse(msg: &M) -> Result { + let mut values = msg.headers().get_all(Self::name()); + let val = if let Some(value) = values.next() { + Some( + usize::from_str(value.to_str().map_err(|_| ParseError::Header)?.trim()) + .map_err(|_| ParseError::Header)?, + ) + } else { + None + }; + if values.next().is_some() { + return Err(ParseError::TooLarge); + } + Ok(Self(val)) + } +} + +impl TryIntoHeaderValue for TtlExpiresAtHeader { + type Error = std::convert::Infallible; + + fn try_into_value(self) -> Result { + Ok(self + .0 + .map(HeaderValue::from) + .unwrap_or(HeaderValue::from_static(""))) + } +} + +impl header::Header for TtlExpiresAtHeader { + fn name() -> HeaderName { + HeaderName::from_static("huly-expire-at") + } + + fn parse(msg: &M) -> Result { + let mut values = msg.headers().get_all(Self::name()); + let val = if let Some(value) = values.next() { + Some( + u64::from_str(value.to_str().map_err(|_| ParseError::Header)?.trim()) + .map_err(|_| ParseError::Header)?, + ) + } else { + None + }; + if values.next().is_some() { + return Err(ParseError::TooLarge); + } + Ok(Self(val)) + } +} diff --git a/foundations/hulypulse/src/handlers_ws.rs b/foundations/hulypulse/src/handlers_ws.rs new file mode 100644 index 0000000000..5aa291c93c --- /dev/null +++ b/foundations/hulypulse/src/handlers_ws.rs @@ -0,0 +1,392 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use actix_ws; +use futures_util::StreamExt; +// use tracing::info; +use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, web}; +use serde::Deserialize; +use serde_json::{Value, json}; +use std::sync::Arc; +use tokio::sync::RwLock; + +use crate::{ + config::CONFIG, + db::Db, + hub_service::{HubState, SessionId, new_session_id}, + redis::{SaveMode, Ttl}, + workspace_owner::check_workspace_core, + workspace_owner::test_rego_claims, +}; + +use strum_macros::AsRefStr; + +#[derive(Deserialize, Debug, AsRefStr)] +#[serde(rename_all = "lowercase", tag = "type")] +pub enum WsCommand { + Put { + correlation: String, + key: String, + data: String, + + #[serde(rename = "expiresAt")] + #[serde(default)] + expires_at: Option, + + #[serde(rename = "TTL")] + #[serde(default)] + ttl: Option, + + #[serde(rename = "ifMatch")] + #[serde(default)] + if_match: Option, + + #[serde(rename = "ifNoneMatch")] + #[serde(default)] + if_none_match: Option, + }, + + Delete { + correlation: String, + key: String, + + #[serde(rename = "ifMatch")] + #[serde(default)] + if_match: Option, + }, + + Get { + correlation: String, + key: String, + }, + + List { + correlation: String, + key: String, + }, + + Sub { + correlation: String, + key: String, + }, + + Unsub { + correlation: String, + key: String, + }, + + Sublist { + correlation: String, + }, + + Info { + correlation: String, + }, +} + +use hulyrs::services::jwt::Claims; + +async fn wrong_workspace( + claims: &Option, + key: &str, + correlation: &String, + session: &mut actix_ws::Session, +) -> bool { + if let Err(e) = check_workspace_core(claims.clone(), key) { + result_err(e, correlation, session).await; + return true; + } + false +} + +async fn result>( + result: T, + correlation: &String, + session: &mut actix_ws::Session, +) { + let _ = session + .text(json!({ "correlation": correlation, "result": result.into()}).to_string()) + .await; +} + +async fn result_err(err: impl Into, correlation: &String, session: &mut actix_ws::Session) { + let _ = session + .text(json!({ "correlation": correlation, "error": err.into()}).to_string()) + .await; +} + +async fn handle_command( + ws: &mut actix_ws::Session, + cmd: WsCommand, + db: &Db, + hub_state: &Arc>, + claims: Option, + session_id: SessionId, +) { + match cmd { + // INFO + WsCommand::Info { correlation } => { + tracing::info!("INFO"); + match db.info().await { + Ok(info) => result(info, &correlation, ws).await, + Err(e) => result_err(e.to_string(), &correlation, ws).await, + } + } + + // PUT + WsCommand::Put { + key, + data, + expires_at, + ttl, + if_match, + if_none_match, + correlation, + } => { + tracing::info!("PUT {} = {}", &key, &data); + if wrong_workspace(&claims, &key, &correlation, ws).await { + return; + } + + // TTL logic + let real_ttl = if let Some(secs) = ttl { + Some(Ttl::Sec(secs as usize)) + } else if let Some(timestamp) = expires_at { + Some(Ttl::At(timestamp)) + } else { + None + }; + + // SaveMode logic + let mut mode = Some(SaveMode::Upsert); + if let Some(s) = if_match { + if s == "*" { + mode = Some(SaveMode::Update); + } else { + mode = Some(SaveMode::Equal(s)); + } + } else if let Some(s) = if_none_match { + if s == "*" { + mode = Some(SaveMode::Insert); + } else { + result_err("ifNoneMatch must contain only '*'", &correlation, ws).await; + return; + } + } + + match db.save(&key, &data, real_ttl, mode).await { + Ok(_) => result("OK", &correlation, ws).await, + Err(e) => result_err(e.to_string(), &correlation, ws).await, + } + } + + WsCommand::Delete { + key, + correlation, + if_match, + } => { + tracing::info!("DELETE {}", &key); // correlation:{:?} , &correlation + if wrong_workspace(&claims, &key, &correlation, ws).await { + return; + } + + // MODE logic + let mut mode = Some(SaveMode::Upsert); + if let Some(s) = if_match { + if s == "*" { + // `If-Match: *` - return error if not exist + mode = Some(SaveMode::Update); + } else { + // `If-Match: ` - delete only if current + mode = Some(SaveMode::Equal(s)); + } + } + + // Delete + match db.delete(&key, mode).await { + Ok(true) => result("OK", &correlation, ws).await, + Ok(false) => result_err("not found", &correlation, ws).await, + Err(e) => result_err(e.to_string(), &correlation, ws).await, + } + } + + WsCommand::Get { key, correlation } => { + tracing::info!("GET {}", &key); + if wrong_workspace(&claims, &key, &correlation, ws).await { + return; + } + + match db.read(&key).await { + Ok(Some(data)) => match serde_json::to_value(&data) { + Ok(v) => result(v, &correlation, ws).await, + Err(e) => result_err(e.to_string(), &correlation, ws).await, + }, + Ok(None) => result_err("not found", &correlation, ws).await, + Err(e) => result_err(e.to_string(), &correlation, ws).await, + } + } + + WsCommand::List { key, correlation } => { + tracing::info!("LIST {:?}", &key); + if wrong_workspace(&claims, &key, &correlation, ws).await { + return; + } + match db.list(&key).await { + Ok(data) => { + let values: Vec = data.into_iter().map(|item| json!(item)).collect(); + result(values, &correlation, ws).await; + } + Err(e) => result_err(e.to_string(), &correlation, ws).await, + } + } + + WsCommand::Sub { key, correlation } => { + tracing::info!("SUB {}", &key); + if wrong_workspace(&claims, &key, &correlation, ws).await { + return; + } + hub_state.write().await.subscribe(session_id, key); + result("OK", &correlation, ws).await; + } + + WsCommand::Unsub { key, correlation } => { + tracing::info!("UNSUB {}", &key); + if key == "*" { + hub_state.write().await.unsubscribe_all(session_id); + result("OK", &correlation, ws).await; + } else { + if wrong_workspace(&claims, &key, &correlation, ws).await { + return; + } + hub_state.write().await.unsubscribe(session_id, key); + result("OK", &correlation, ws).await; + } + } + + WsCommand::Sublist { correlation } => { + tracing::info!("SUBLIST"); + // w/o Check workspace! + let keys = hub_state.read().await.subscribe_list(session_id); + result(keys, &correlation, ws).await; + } // End of commands + } +} +// } + +pub async fn handler( + req: HttpRequest, + payload: web::Payload, + db: web::Data, + hub_state: web::Data>>, +) -> Result { + let claims = if !CONFIG.no_authorization { + Some( + req.extensions() + .get::() + .expect("Missing claims") + .to_owned(), + ) + } else { + None + }; + + let (response, mut session, mut msg_stream) = actix_ws::handle(&req, payload)?; + + let session_id = new_session_id(); + + hub_state.write().await.connect(session_id, session.clone()); + tracing::info!("WebSocket connected: {}", session_id); + + actix_web::rt::spawn(async move { + while let Some(Ok(msg)) = msg_stream.next().await { + tracing::debug!("WebSocket message: {:?}", msg); + + // renew heartbeat to unixtime (all messages is activity, including "ping") + hub_state.write().await.renew_heartbeat(session_id); + + match msg { + actix_ws::Message::Ping(bytes) => { + session.pong(&bytes).await.ok(); + continue; + } + + actix_ws::Message::Pong(_) => { + continue; + } + + actix_ws::Message::Text(text) if text == "ping" => { + let _ = session.text("pong").await; + continue; + } + actix_ws::Message::Text(text) if text == "pong" => { + continue; + } + + actix_ws::Message::Text(text) => match serde_json::from_str::(&text) { + Ok(cmd) => { + if !CONFIG.no_authorization { + let key = match &cmd { + WsCommand::Put { key, .. } + | WsCommand::Delete { key, .. } + | WsCommand::Get { key, .. } + | WsCommand::List { key, .. } + | WsCommand::Sub { key, .. } + | WsCommand::Unsub { key, .. } => key.as_str(), + _ => "", + }; + + if let Some(ref claim) = claims { + if !test_rego_claims(claim, cmd.as_ref(), key) { + let _ = session.text("Unauthorized: Rego policy").await; + break; + } + } + } + + handle_command( + &mut session, + cmd, + &db, + &hub_state, + claims.clone(), + session_id, + ) + .await; + } + + Err(err) => { + let _ = session.text(format!("Invalid JSON: {}", err)).await; + } + }, + + actix_ws::Message::Close(reason) => { + if let Err(e) = session.close(reason).await { + tracing::warn!("WS close error: {:?}", e); + } + break; + } + + _ => { + tracing::info!("Unhandled WS message: {:?}", msg); + } + } + } + + hub_state.write().await.disconnect(session_id); + tracing::info!("WebSocket disconnected: {}", session_id); + }); + + Ok(response) +} diff --git a/foundations/hulypulse/src/hub_service.rs b/foundations/hulypulse/src/hub_service.rs new file mode 100644 index 0000000000..38082a8566 --- /dev/null +++ b/foundations/hulypulse/src/hub_service.rs @@ -0,0 +1,250 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use crate::config::CONFIG; +use redis::aio::MultiplexedConnection; +use serde::Serialize; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use tokio::sync::RwLock; + +fn subscription_matches(sub_key: &str, key: &str) -> bool { + if sub_key == key { + return true; + } + if sub_key.ends_with('/') && key.starts_with(sub_key) { + let rest = &key[sub_key.len()..]; + return !rest.contains('$'); + } + false +} + +#[derive(Clone, Serialize, Debug)] +pub struct ServerMessage { + #[serde(flatten)] + pub event: RedisEvent, + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, +} + +pub type SessionId = u64; +static NEXT_ID: AtomicU64 = AtomicU64::new(1); +pub fn new_session_id() -> SessionId { + NEXT_ID.fetch_add(1, Ordering::SeqCst) +} + +#[derive(Debug, Clone, Serialize)] +pub enum RedisEventAction { + Set, + Del, + Unlink, + Expired, + Other(String), +} + +#[derive(Debug, Clone, Serialize)] +pub struct RedisEvent { + // pub db: u32, + pub message: RedisEventAction, + pub key: String, +} + +#[derive(Default)] // Debug, +pub struct HubState { + sessions: HashMap, + subs: HashMap>, + heartbeats: HashMap, + serverping: HashMap, +} + +impl HubState { + pub fn renew_heartbeat(&mut self, session_id: SessionId) { + if self.sessions.contains_key(&session_id) { + let now = std::time::Instant::now(); + self.heartbeats.insert(session_id, now); + self.serverping.insert(session_id, now); + } + } + + pub fn connect(&mut self, session_id: SessionId, session: actix_ws::Session) { + self.sessions.insert(session_id, session); + self.heartbeats + .insert(session_id, std::time::Instant::now()); + self.serverping + .insert(session_id, std::time::Instant::now()); + } + + pub fn disconnect(&mut self, session_id: SessionId) { + self.sessions.remove(&session_id); + self.heartbeats.remove(&session_id); + self.serverping.remove(&session_id); + self.subs.retain(|_, ids| { + ids.remove(&session_id); + !ids.is_empty() + }); + } + + pub fn subscribe(&mut self, session_id: SessionId, key: String) { + self.subs.entry(key).or_default().insert(session_id); + } + + pub fn unsubscribe(&mut self, session_id: SessionId, key: String) { + if let Some(set) = self.subs.get_mut(&key) { + set.remove(&session_id); + if set.is_empty() { + self.subs.remove(&key); + } + } + } + pub fn unsubscribe_all(&mut self, session_id: SessionId) { + self.subs.retain(|_, ids| { + ids.remove(&session_id); + !ids.is_empty() + }); + } + + pub fn subscribe_list(&self, session_id: SessionId) -> Vec { + self.subs + .iter() + .filter_map(|(key, ids)| { + if ids.contains(&session_id) { + Some(key.clone()) + } else { + None + } + }) + .collect() + } + + pub fn count(&self) -> usize { + self.sessions.len() + } + + pub fn recipients_for_key(&self, key: &str) -> Vec { + let mut out = Vec::new(); + for (sub_key, set) in &self.subs { + if subscription_matches(sub_key, key) { + for sid in set { + if let Some(r) = self.sessions.get(sid) { + out.push(r.clone()); + } + } + } + } + out + } +} + +// Send messages about new db events +pub async fn broadcast_event( + hub_state: &Arc>, + ev: RedisEvent, + value: Option, +) { + // Collect + let recipients: Vec = { hub_state.read().await.recipients_for_key(&ev.key) }; + if recipients.is_empty() { + return; + } + + // Send + let payload = ServerMessage { event: ev, value }; + for mut rcpt in recipients { + // let _ = rcpt.do_send(payload.clone()); + let json = serde_json::to_string(&payload).unwrap(); + let _ = rcpt.text(json).await; + } +} + +pub async fn push_event( + hub_state: &Arc>, + redis: &mut MultiplexedConnection, + ev: RedisEvent, +) { + // Value only for Set + let mut value: Option = None; + if matches!(ev.message, RedisEventAction::Set) { + match ::redis::cmd("GET") + .arg(&ev.key) + .query_async::>(redis) + .await + { + Ok(v) => value = v, + Err(e) => tracing::warn!("redis GET {} failed: {}", &ev.key, e), + } + } + + broadcast_event(hub_state, ev, value).await; +} + +pub fn check_heartbeat(hub_state: Arc>) { + tokio::spawn(async move { + let mut ticker = tokio::time::interval(std::time::Duration::from_secs(2)); + loop { + ticker.tick().await; + + let now = std::time::Instant::now(); + let timelimit = now - std::time::Duration::from_secs(CONFIG.heartbeat_timeout); + let pinglimit = now - std::time::Duration::from_secs(CONFIG.ping_timeout); + + let hub = hub_state.read().await; + + let expired: Vec = hub + .heartbeats + .iter() + .filter_map(|(&sid, &last_beat)| { + if last_beat < timelimit { + hub.sessions.get(&sid).cloned() + } else { + None + } + }) + .collect(); + + let to_ping: Vec = hub + .serverping + .iter() + .filter_map(|(&sid, &last_ping)| { + if last_ping < pinglimit { + Some(sid) + } else { + None + } + }) + .collect(); + + drop(hub); + + if !expired.is_empty() { + for addr in &expired { + // addr.do_send(crate::handlers_ws::ForceDisconnect); + let _ = addr.clone().close(None).await; + } + } + + if !to_ping.is_empty() { + let mut hub = hub_state.write().await; + for sid in &to_ping { + if let Some(session) = hub.sessions.get_mut(sid) { + let _ = session.ping(&[]).await; + } + hub.serverping.insert(*sid, now); + } + drop(hub); + } + } + }); +} diff --git a/foundations/hulypulse/src/main.rs b/foundations/hulypulse/src/main.rs new file mode 100644 index 0000000000..203c6280c1 --- /dev/null +++ b/foundations/hulypulse/src/main.rs @@ -0,0 +1,204 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use actix_cors::Cors; +use actix_web::{ + App, Error, HttpMessage, HttpResponse, HttpServer, + body::MessageBody, + dev::{ServiceRequest, ServiceResponse}, + middleware::{self, Next}, + web::{self, Path, Query}, +}; + +use hulyrs::services::jwt::{Claims, actix::ServiceRequestExt}; +use secrecy::ExposeSecret; +use serde_json::json; +use tracing::*; +use uuid::Uuid; + +mod config; +mod handlers_http; +mod handlers_ws; +mod redis; +mod workspace_owner; + +mod hub_service; +use hub_service::HubState; + +use config::CONFIG; + +mod db; +mod memory; + +use crate::memory::MemoryBackend; +use crate::{db::Db, hub_service::check_heartbeat}; + +fn initialize_tracing(level: tracing::Level) { + use tracing_subscriber::{filter::targets::Targets, prelude::*}; + + let filter = Targets::default() + .with_target(env!("CARGO_BIN_NAME"), level) + .with_target("actix", tracing::Level::WARN); + let format = tracing_subscriber::fmt::layer().compact(); + + tracing_subscriber::registry() + .with(filter) + .with(format) + .init(); +} + +async fn extract_claims( + mut request: ServiceRequest, + next: Next, +) -> Result, Error> { + #[derive(serde::Deserialize)] + struct QueryString { + token: Option, + } + + if !CONFIG.no_authorization { + let query = request.extract::>().await?.into_inner(); + + let claims = if let Some(token) = query.token { + Claims::from_token(token, CONFIG.token_secret.expose_secret()).unwrap() + } else { + request.extract_claims(&CONFIG.token_secret)? + }; + request.extensions_mut().insert(claims); + } + + next.call(request).await +} + +async fn check_workspace( + mut request: ServiceRequest, + next: Next, +) -> Result, Error> { + if !CONFIG.no_authorization { + let workspace = Uuid::parse_str(&request.extract::>().await?); + let claims = request.extensions().get::().cloned().unwrap(); + + if claims.is_system() || Ok(claims.workspace.clone()) == workspace.clone().map(Some) { + next.call(request).await + } else { + warn!( + expected = ?claims.workspace, + actual = ?workspace, + "Unauthorized request, workspace mismatch" + ); + Err(actix_web::error::ErrorUnauthorized("Unauthorized").into()) + } + } else { + next.call(request).await + } +} + +#[actix_web::main] +async fn main() -> anyhow::Result<()> { + initialize_tracing(tracing::Level::TRACE); + + tracing::info!("{}/{}", env!("CARGO_BIN_NAME"), env!("CARGO_PKG_VERSION")); + + // starting HubService + let hub_state = Arc::new(RwLock::new(HubState::default())); + + // starting heartbeat checker + check_heartbeat(hub_state.clone()); + + let db_backend = match CONFIG.backend { + config::BackendType::Memory => { + let memory = MemoryBackend::new(); + memory.spawn_ticker(hub_state.clone()); + tracing::info!("Memory mode enabled"); + Db::new_memory(memory, hub_state.clone()) + } + + config::BackendType::Redis => { + let redis_client = redis::client().await?; + let redis_connection = redis_client.get_multiplexed_async_connection().await?; + tokio::spawn(crate::redis::receiver(redis_client, hub_state.clone())); + tracing::info!("Redis mode enabled"); + Db::new_redis(redis_connection, hub_state.clone()) + } + }; + + let socket = std::net::SocketAddr::new(CONFIG.bind_host.as_str().parse()?, CONFIG.bind_port); + + let url = format!("http://{}:{}", &CONFIG.bind_host, &CONFIG.bind_port); + tracing::info!("Server running at {}", &url); + tracing::info!("HTTP API: {}/api", &url); + tracing::info!("WebSocket API: {}/ws", &url); + tracing::info!("Status: {}/status", &url); + + use std::sync::Arc; + use tokio::sync::RwLock; + + let server = HttpServer::new(move || { + let cors = Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header() + .supports_credentials() + .max_age(3600); + + App::new() + .app_data(web::Data::new(db_backend.clone())) + .app_data(web::Data::new(hub_state.clone())) + .wrap(middleware::Logger::default()) + .wrap(cors) + .service( + web::scope("/api/{workspace}") + .wrap(middleware::from_fn(check_workspace)) + .wrap(middleware::from_fn(extract_claims)) + .route("/{key:.+/}", web::get().to(handlers_http::list)) + .route("/{key:.+}", web::get().to(handlers_http::get)) + .route("/{key:.+}", web::put().to(handlers_http::put)) + .route("/{key:.+}", web::delete().to(handlers_http::delete)), + ) + .route( + "/ws", + web::get() + .to(handlers_ws::handler) + .wrap(middleware::from_fn(extract_claims)), + ) // WebSocket + .route( + "/status", + web::get().to({ + move |hub_state: web::Data>>, db_backend: web::Data| { + let hub_state = hub_state.clone(); + async move { + let info = db_backend + .info() + .await + .unwrap_or_else(|_| "error".to_string()); + let count = hub_state.read().await.count(); + Ok::<_, actix_web::Error>(HttpResponse::Ok().json(json!({ + "memory_info": info, + "backend": CONFIG.backend.to_string().to_lowercase(), + "websockets": count, + "status": "OK", + }))) + } + } + }), + ) + }) + .bind(socket)? + .run(); + + server.await?; + + Ok(()) +} diff --git a/foundations/hulypulse/src/memory.rs b/foundations/hulypulse/src/memory.rs new file mode 100644 index 0000000000..4774f36980 --- /dev/null +++ b/foundations/hulypulse/src/memory.rs @@ -0,0 +1,344 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use crate::{ + config::CONFIG, + hub_service::{HubState, RedisEvent, RedisEventAction, broadcast_event}, + redis::{RedisArray, SaveMode, Ttl, deprecated_symbol_error, error}, +}; +use std::{ + collections::HashMap, + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; +use tokio::{ + sync::RwLock, + time::{self, Duration}, +}; + +#[derive(Debug, Clone)] +struct Entry { + data: String, + ttl: u8, +} + +#[derive(Clone, Default)] +pub struct MemoryBackend { + inner: Arc>>, + tick: Arc>, // counter +} + +impl MemoryBackend { + pub fn new() -> Self { + Self { + inner: Arc::new(RwLock::new(HashMap::new())), + tick: Arc::new(RwLock::new(0)), + } + } + + pub fn spawn_ticker(&self, hub_state: Arc>) { + let inner = self.inner.clone(); + let tick = self.tick.clone(); + + tokio::spawn(async move { + let mut ticker = time::interval(Duration::from_secs(1)); + loop { + ticker.tick().await; + + let current_tick = { + let mut t = tick.write().await; + *t = t.wrapping_add(1); + *t + }; + + let expired_keys: Vec = { + let map = inner.read().await; + map.iter() + .filter(|(_, v)| v.ttl == current_tick) + .map(|(k, _)| k.clone()) + .collect() + }; + + { + let mut map = inner.write().await; + for k in &expired_keys { + map.remove(k); + } + } + + for k in expired_keys { + broadcast_event( + &hub_state, + RedisEvent { + message: RedisEventAction::Expired, + key: k, + }, + None, + ) + .await; + } + } + }); + } +} + +/// memory_list(&backend, "prefix/") → Vec +pub async fn memory_list( + backend: &MemoryBackend, + key_prefix: &str, +) -> redis::RedisResult> { + deprecated_symbol_error(key_prefix)?; + if !key_prefix.ends_with('/') { + return error(412, "Key must end with slash"); + } + + let map = backend.inner.read().await; + let current_tick = *backend.tick.read().await; + + let mut results = Vec::new(); + for (k, v) in map.iter() { + if !k.starts_with(key_prefix) { + continue; + } + + if k.strip_prefix(key_prefix) + .map_or(false, |s| s.contains('$')) + { + continue; + } + + if v.ttl == 0 { + continue; + } + + let expires = v.ttl.wrapping_sub(current_tick); + + results.push(RedisArray { + key: k.clone(), + data: v.data.clone(), + ttl: expires as u64, + etag: hex::encode(md5::compute(&v.data).0), + }); + } + + Ok(results) +} + +/// memory_info(&backend) +pub async fn memory_info(backend: &MemoryBackend) -> redis::RedisResult { + let map = backend.inner.read().await; + let keys = map.len(); + let memory: usize = map.values().map(|v| v.data.len()).sum(); + Ok(format!("{} keys, {} bytes", keys, memory)) +} + +/// memory_read(&backend, "key") +pub async fn memory_read( + backend: &MemoryBackend, + key: &str, +) -> redis::RedisResult> { + deprecated_symbol_error(key)?; + if key.ends_with('/') { + return error(412, "Key must not end with a slash"); + } + + let map = backend.inner.read().await; + + match map.get(key) { + None => Ok(None), + Some(entry) => { + let data = entry.data.clone(); + let current_tick = *backend.tick.read().await; + let expires = entry.ttl.wrapping_sub(current_tick); + + Ok(Some(RedisArray { + key: key.to_string(), + data: data.clone(), + ttl: expires as u64, + etag: hex::encode(md5::compute(&data).0), + })) + } + } +} + +/// TTL in sec +fn compute_ttl_u8(ttl: Option) -> redis::RedisResult { + let sec_usize = match ttl { + Some(Ttl::Sec(secs)) => secs, + Some(Ttl::At(timestamp)) => { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + if timestamp <= now { + return error(400, "TTL timestamp exceeds MAX_TTL limit"); + } + (timestamp - now) as usize + } + None => CONFIG.max_ttl, + }; + + if sec_usize == 0 { + return error(400, "TTL must be > 0"); + } + + if sec_usize > CONFIG.max_ttl || sec_usize > 255 { + return error(412, "TTL exceeds MAX_TTL or 255 sec"); + } + + let capped = sec_usize.min(u8::MAX as usize); + Ok(capped as u8) +} + +/// memory_save(&backend, "key", value, ttl, mode) +pub async fn memory_save>( + backend: &MemoryBackend, + key: &str, + bytes_value: V, + ttl: Option, + mode: Option, +) -> redis::RedisResult<()> { + // u8 - String + let value = match std::str::from_utf8(bytes_value.as_ref()) { + Ok(s) => s.to_string(), + Err(_) => return error(400, "Value must be valid UTF-8"), + }; + + // If max_size != 0 and value size > max_size, return error + let max_size = CONFIG.max_size.unwrap_or(0); + if max_size != 0 && value.len() > max_size { + return error( + 400, + format!("Value in memory mode must be less than {} bytes", max_size), + ); + } + + deprecated_symbol_error(key)?; + if key.ends_with('/') { + return error(412, "Key must not end with a slash"); + } + + let ttl_u8 = compute_ttl_u8(ttl)?; + let current_tick = *backend.tick.read().await; + let expire_tick = current_tick.wrapping_add(ttl_u8); + + let val = value.to_string(); + + let mut map = backend.inner.write().await; + + let mode = mode.unwrap_or(SaveMode::Upsert); + + match mode { + SaveMode::Upsert => { + map.insert( + key.to_string(), + Entry { + data: val, + ttl: expire_tick, + }, + ); + } + SaveMode::Insert => { + if map.contains_key(key) { + return error(412, "Insert: key already exists"); + } + map.insert( + key.to_string(), + Entry { + data: val, + ttl: expire_tick, + }, + ); + } + SaveMode::Update => { + let Some(existing) = map.get_mut(key) else { + return error(404, "Update: key does not exist"); + }; + *existing = Entry { + data: val, + ttl: expire_tick, + }; + } + SaveMode::Equal(ref expected_md5) => { + let Some(existing) = map.get_mut(key) else { + return error(404, "Equal: key does not exist"); + }; + let actual_md5 = hex::encode(md5::compute(&existing.data).0); + if &actual_md5 != expected_md5 { + return error( + 412, + format!( + "md5 mismatch, current: {}, expected: {}", + actual_md5, expected_md5 + ), + ); + } + *existing = Entry { + data: val, + ttl: expire_tick, + }; + } + } + + Ok(()) +} + +/// memory_delete(&backend, "key", mode) +pub async fn memory_delete( + backend: &MemoryBackend, + key: &str, + mode: Option, +) -> redis::RedisResult { + deprecated_symbol_error(key)?; + if key.ends_with('/') { + return error(412, "Key must not end with a slash"); + } + + let mut map = backend.inner.write().await; + let mode = mode.unwrap_or(SaveMode::Upsert); + + match mode { + SaveMode::Insert => { + return error(412, "Insert mode is not supported for delete"); + } + SaveMode::Update | SaveMode::Upsert => { + let existed = map.remove(key).is_some(); + Ok(existed) + } + SaveMode::Equal(ref expected_md5) => { + match map.get(key) { + None => return error(404, "Equal: key does not exist"), + Some(existing) => { + let actual_md5 = hex::encode(md5::compute(&existing.data).0); + if &actual_md5 != expected_md5 { + return error( + 412, + format!( + "md5 mismatch, current: {}, expected: {}", + actual_md5, expected_md5 + ), + ); + } + } + } + let existed = map.remove(key).is_some(); + if !existed { + // WHF?! + return error(404, "Delete: key does not exist"); + } + Ok(true) + } + } +} diff --git a/foundations/hulypulse/src/pulse-status.sh b/foundations/hulypulse/src/pulse-status.sh new file mode 100755 index 0000000000..cadc091b2d --- /dev/null +++ b/foundations/hulypulse/src/pulse-status.sh @@ -0,0 +1 @@ +curl -i https://pulse.hc.engineering/status \ No newline at end of file diff --git a/foundations/hulypulse/src/redis.rs b/foundations/hulypulse/src/redis.rs new file mode 100644 index 0000000000..cb230e247e --- /dev/null +++ b/foundations/hulypulse/src/redis.rs @@ -0,0 +1,533 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use std::{ + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; + +use ::redis::Msg; +use tokio::sync::RwLock; +use tokio_stream::StreamExt; +use tracing::*; + +use crate::{ + config::{CONFIG, RedisMode}, + hub_service::{HubState, RedisEvent, RedisEventAction, push_event}, +}; + +#[derive(serde::Serialize)] +pub enum Ttl { + Sec(usize), // EX + At(u64), // EXAT (timestamp in seconds) +} + +#[derive(Debug)] +pub enum SaveMode { + Upsert, // default: set or overwrite + Insert, // only if not exists (NX) + Update, // only if exists (XX) + Equal(String), // only if md5 matches provided +} + +use redis::{ + Client, ConnectionInfo, ProtocolVersion, RedisConnectionInfo, RedisResult, ToRedisArgs, + aio::MultiplexedConnection, +}; +use serde::Serialize; + +static MAX_LOOP_COUNT: usize = 1000; // to avoid infinite loops + +#[derive(Debug, Serialize)] +pub struct RedisArray { + pub key: String, + pub data: String, + pub ttl: u64, // sec to expire TTL + pub etag: String, // md5 hash (data) +} + +/// return Error +pub fn error(code: u16, msg: impl Into) -> redis::RedisResult { + let msg = msg.into(); + let full = format!("{}: {}", code, msg); + Err(redis::RedisError::from(( + redis::ErrorKind::ExtensionError, + "", + full, + ))) +} + +/// Check for redis-deprecated symbols +pub fn deprecated_symbol(s: &str) -> bool { + s.chars().any(|c| { + matches!( + c, + '*' | '?' | '[' | ']' | '\\' | '\0'..='\x1F' | '\x7F' | '"' | '\'' + ) + }) +} + +pub fn deprecated_symbol_error(s: &str) -> redis::RedisResult<()> { + if deprecated_symbol(s) { + error(412, "Deprecated symbol in key") + } else { + Ok(()) + } +} + +/// redis_info(&connection) +pub async fn redis_info(conn: &mut MultiplexedConnection) -> redis::RedisResult { + let info: String = redis::cmd("INFO").query_async(conn).await?; + + let mut redis_keys: Option = None; + let mut redis_bytes: Option = None; + + for line in info.lines() { + if line.starts_with("db0:") { + // parsing: db0:keys=152,expires=10,avg_ttl=456789 + if let Some(keys_part) = line.split(',').find(|s| s.starts_with("keys=")) { + if let Some(val) = keys_part.strip_prefix("keys=") { + redis_keys = val.parse::().ok(); + } + } + } + if line.starts_with("used_memory:") { + if let Some(val) = line.strip_prefix("used_memory:") { + redis_bytes = val.parse::().ok(); + } + } + } + + Ok(format!( + "{} keys, {} bytes", + redis_keys.unwrap_or(0), + redis_bytes.unwrap_or(0) + )) +} + +/// redis_list(&connection,prefix) +pub async fn redis_list( + conn: &mut MultiplexedConnection, + key: &str, +) -> redis::RedisResult> { + deprecated_symbol_error(key)?; + if !key.ends_with('/') { + return error(412, "Key must end with slash"); + } + let pattern = format!("{key}*"); + + let mut cursor = 0u64; + let mut results = Vec::new(); + + loop { + let mut cmd = redis::cmd("SCAN"); + cmd.arg(cursor); + cmd.arg("MATCH").arg(&pattern); + // cmd.arg("COUNT").arg(100); // Optionally adjust batch size + + let (next_cursor, keys): (u64, Vec) = cmd.query_async(conn).await?; + + for k in keys { + // Check for $-security path + if k.strip_prefix(key).map_or(false, |s| s.contains('$')) { + continue; + } + + // Get value + let value: Option = redis::cmd("GET").arg(&k).query_async(conn).await?; + let Some(value) = value else { + continue; + }; // Old and deleted + + // Get TTL + let ttl: i64 = redis::cmd("TTL").arg(&k).query_async(conn).await?; + if ttl >= 0 { + results.push(RedisArray { + key: k, + data: value.clone(), + ttl: ttl as u64, + etag: hex::encode(md5::compute(&value).0), + }); + } + } + + if next_cursor == 0 { + break; + } + cursor = next_cursor; + } + + Ok(results) +} + +/// redis_read(&connection,key) +pub async fn redis_read( + conn: &mut MultiplexedConnection, + key: &str, +) -> redis::RedisResult> { + deprecated_symbol_error(key)?; + + if key.ends_with('/') { + return error(412, "Key must not end with a slash"); + } + + let data: Option = redis::cmd("GET").arg(key).query_async(conn).await?; + + let Some(data) = data else { + return Ok(None); + }; + + let ttl: i64 = redis::cmd("TTL").arg(key).query_async(conn).await?; + match ttl { + -1 => return error(500, "TTL not set"), + -2 => return error(500, "Key not found"), + x if x < 0 => return error(500, "Unknown TTL error"), + _ => {} // ttl >= 0, ок + } + + Ok(Some(RedisArray { + key: key.to_string(), + data: data.clone(), + ttl: ttl as u64, + etag: hex::encode(md5::compute(&data).0), + })) +} + +/// redis_save(&connection,key,value,[ttl?],[mode?]) +pub async fn redis_save( + conn: &mut MultiplexedConnection, + key: &str, + value: T, + ttl: Option, + mode: Option, +) -> RedisResult<()> { + deprecated_symbol_error(&key)?; + + if key.ends_with('/') { + return error(412, "Key must not end with a slash"); + } + + // If max_size != 0 and value size > max_size, return error + let max_size = CONFIG.max_size.unwrap_or(0); + if max_size != 0 && value.to_redis_args().iter().map(|a| a.len()).sum::() > max_size { + return error( + 400, + format!("Value in memory mode must be less than {} bytes", max_size), + ); + } + + // TTL logic + let sec = match ttl { + Some(Ttl::Sec(secs)) => secs, + Some(Ttl::At(timestamp)) => { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + if timestamp <= now { + return error(400, "TTL timestamp exceeds MAX_TTL limit"); + } + (timestamp - now) as usize + } + None => CONFIG.max_ttl, + }; + if sec == 0 { + return error(400, "TTL must be > 0"); + } + if sec > CONFIG.max_ttl { + return error(412, "TTL exceeds MAX_TTL"); + } + + let mode = mode.unwrap_or(SaveMode::Upsert); + + match mode { + SaveMode::Upsert | SaveMode::Insert | SaveMode::Update => { + let mut cmd = redis::cmd("SET"); + cmd.arg(key).arg(value).arg("EX").arg(sec); + match mode { + SaveMode::Insert => { + cmd.arg("NX"); + } // if NOT Exist + SaveMode::Update => { + cmd.arg("XX"); + } // if Exist + _ => {} + }; + let result: Option = cmd.query_async(conn).await?; + if result.is_none() { + return match mode { + SaveMode::Insert => error(412, "Insert: key already exists"), + SaveMode::Update => error(404, "Update: key does not exist"), + _ => error(500, "Unexpected Redis SET failure"), + }; + } + Ok(()) + } + + SaveMode::Equal(ref expected_md5) => { + let mut loop_count = 0; + loop { + let _: () = redis::cmd("WATCH").arg(key).query_async(conn).await?; + + let current: Option = redis::cmd("GET").arg(key).query_async(conn).await?; + let existing = match current { + None => { + let _: () = redis::cmd("UNWATCH").query_async(conn).await?; + return error(404, "Equal: key does not exist"); + } + Some(v) => v, + }; + // check md5 + let actual_md5 = hex::encode(md5::compute(&existing).0); + if &actual_md5 != expected_md5 { + let _: () = redis::cmd("UNWATCH").query_async(conn).await?; + return error( + 412, + format!( + "md5 mismatch, current: {}, expected: {}", + actual_md5, expected_md5 + ), + ); + } + + // MULTI/EXEC + let mut pipe = redis::pipe(); + pipe.atomic() + .cmd("SET") + .arg(key) + .arg(value.to_redis_args()) + .arg("EX") + .arg(sec); + + let result: Option = pipe.query_async(conn).await?; + if result.is_some() { + break; + } + // None -> key was changed -> repeat loop + loop_count += 1; + if loop_count > MAX_LOOP_COUNT { + let _: () = redis::cmd("UNWATCH").query_async(conn).await?; + return error(500, "Something wrong: too many retries on Equal mode"); + } + } + + Ok(()) + } + } +} + +/// redis_delete(&connection,key) +pub async fn redis_delete( + conn: &mut MultiplexedConnection, + key: &str, + mode: Option, +) -> RedisResult { + deprecated_symbol_error(key)?; + + if key.ends_with('/') { + return error(412, "Key must not end with a slash"); + } + + let mode = mode.unwrap_or(SaveMode::Upsert); + + match mode { + SaveMode::Update | SaveMode::Upsert => { + let deleted: i32 = redis::cmd("DEL").arg(key).query_async(conn).await?; + return Ok(deleted > 0); + } + + SaveMode::Equal(ref expected_md5) => { + let mut loop_count = 0; + loop { + let _: () = redis::cmd("WATCH").arg(key).query_async(conn).await?; + + let current: Option = redis::cmd("GET").arg(key).query_async(conn).await?; + let existing = match current { + None => { + let _: () = redis::cmd("UNWATCH").query_async(conn).await?; + return error(404, "Equal: key does not exist"); + } + Some(val) => val, + }; + + // check md5 + let actual_md5 = hex::encode(md5::compute(&existing).0); + if &actual_md5 != expected_md5 { + let _: () = redis::cmd("UNWATCH").query_async(conn).await?; + return error( + 412, + format!( + "md5 mismatch, current: {}, expected: {}", + actual_md5, expected_md5 + ), + ); + } + + let mut pipe = redis::pipe(); + pipe.atomic().cmd("DEL").arg(key); + + let deleted: Option = pipe.query_async(conn).await?; + if let Some(n) = deleted { + return Ok(n > 0); + } + // None -> key was changed -> repeat loop + loop_count += 1; + if loop_count > MAX_LOOP_COUNT { + let _: () = redis::cmd("UNWATCH").query_async(conn).await?; + return error(500, "Something wrong: too many retries on Equal mode"); + } + } + } + + SaveMode::Insert => { + return error(412, "Insert mode is not supported for delete"); + } + } +} + +impl TryFrom for RedisEvent { + type Error = anyhow::Error; + + fn try_from(msg: Msg) -> Result { + let channel = match msg.get_channel::() { + Ok(c) => c, + Err(e) => { + anyhow::bail!("[redis_events] bad channel: {e}"); + } + }; + let payload = match msg.get_payload::() { + Ok(p) => p, + Err(e) => { + anyhow::bail!("[redis_events] bad payload: {e}"); + } + }; + + // parsing: "__keyevent@0__:set" → event="set", db=0; payload = key + let event = channel.rsplit(':').next().unwrap_or(""); + let message = match event { + "set" => RedisEventAction::Set, + "del" => RedisEventAction::Del, + "unlink" => RedisEventAction::Unlink, + "expired" => RedisEventAction::Expired, + other => RedisEventAction::Other(other.to_string()), + }; + + Ok(RedisEvent { + // db, + key: payload.clone(), + message, + }) + } +} + +pub async fn receiver( + redis_client: Client, + hub_state: Arc>, +) -> anyhow::Result<()> { + let mut redis = redis_client.get_multiplexed_async_connection().await?; + let mut pubsub = redis_client.get_async_pubsub().await?; + + let _: String = ::redis::cmd("CONFIG") + .arg("SET") + .arg("notify-keyspace-events") + .arg("E$gx") + .query_async(&mut redis) + .await?; + + for pattern in [ + "__keyevent@*__:set", + "__keyevent@*__:del", + "__keyevent@*__:unlink", + "__keyevent@*__:expired", + ] { + pubsub.psubscribe(pattern).await?; + } + + let mut messages = pubsub.on_message(); + + while let Some(message) = messages.next().await { + match RedisEvent::try_from(message) { + Ok(ev) => { + push_event(&hub_state, &mut redis, ev).await; + } + Err(e) => { + warn!("invalid redis message: {e}"); + } + } + } + + Ok(()) +} + +/// redis_connect() +pub async fn client() -> anyhow::Result { + let default_port = match CONFIG.redis_mode { + RedisMode::Sentinel => 6379, + RedisMode::Direct => 6380, + }; + + let urls = CONFIG + .redis_urls + .iter() + .map(|url| { + redis::ConnectionAddr::Tcp( + url.host().unwrap().to_string(), + url.port().unwrap_or(default_port), + ) + }) + .collect::>(); + + if CONFIG.redis_mode == RedisMode::Sentinel { + use redis::sentinel::{SentinelClientBuilder, SentinelServerType}; + + debug!(urls=?urls, service=CONFIG.redis_service, "sentinel configuration"); + + let mut sentinel = SentinelClientBuilder::new( + urls, + CONFIG.redis_service.to_owned(), + SentinelServerType::Master, + ) + .unwrap() + .set_client_to_redis_protocol(ProtocolVersion::RESP3) + .set_client_to_redis_db(11) + .set_client_to_redis_password(CONFIG.redis_password.clone()) + .set_client_to_sentinel_password(CONFIG.redis_password.clone()) + .build()?; + + let client = sentinel.async_get_client().await?; + + Ok(client) + } else { + let single = urls + .first() + .ok_or_else(|| anyhow::anyhow!("No redis URL provided"))?; + + let redis_connection_info = RedisConnectionInfo { + db: 0, + username: None, + password: Some(CONFIG.redis_password.clone()), + protocol: ProtocolVersion::RESP3, + }; + + let connection_info = ConnectionInfo { + addr: single.clone(), + redis: redis_connection_info, + }; + + let client = Client::open(connection_info)?; + + Ok(client) + } +} diff --git a/foundations/hulypulse/src/workspace_owner.rs b/foundations/hulypulse/src/workspace_owner.rs new file mode 100644 index 0000000000..d477abfef6 --- /dev/null +++ b/foundations/hulypulse/src/workspace_owner.rs @@ -0,0 +1,105 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use actix_web::{HttpMessage, HttpRequest}; +use hulyrs::services::jwt::Claims; +use serde_json::json; +use std::{fs, path::Path, sync::LazyLock}; +use uuid::Uuid; + +use crate::{config::CONFIG, redis::deprecated_symbol}; + +// common checker +pub fn check_workspace_core(claims_opt: Option, key: &str) -> Result<(), &'static str> { + if deprecated_symbol(key) { + return Err("Invalid key: deprecated symbols"); + } + + if CONFIG.no_authorization { + return Ok(()); + } + + let claims = claims_opt.ok_or("Missing authorization")?; + + if claims.is_system() { + return Ok(()); + } + + let jwt_workspace = claims + .workspace + .as_ref() + .ok_or("Missing workspace in token")?; + let path_ws = key + .split('/') + .next() + .ok_or("Invalid key: missing workspace")?; + if path_ws.is_empty() { + return Err("Invalid key: missing workspace"); + } + + let path_ws_uuid = Uuid::parse_str(path_ws).map_err(|_| "Invalid workspace UUID in key")?; + if jwt_workspace != &path_ws_uuid { + return Err("Workspace mismatch"); + } + + Ok(()) +} + +pub fn test_rego_claims(claim: &Claims, command: &str, key: &str) -> bool { + let data = serde_json::to_value(&claim).unwrap_or_default(); + let mut rego = REGORUS_ENGINE.clone(); + + rego.set_input(regorus::Value::from(json!({ + "command": command, + "claim": data, + "key": key, + }))); + let result = rego.eval_rule(String::from("data.main.permit")).unwrap(); + + result == regorus::Value::Bool(true) +} + +pub fn test_rego_http(req: HttpRequest, command: &str, key: &str) -> bool { + let claims = req + .extensions() + .get::() + .expect("Missing claims") + .to_owned(); + test_rego_claims(&claims, command, key) +} + +pub static POLICY_TEXT: LazyLock = LazyLock::new(|| { + let Some(policy_file) = CONFIG.policy_file.as_ref() else { + return "package main\n\ndefault permit = true\n".to_string(); + }; + let path = Path::new(policy_file); + if !path.exists() { + panic!("Policy file not found: {}", path.display()); + } + let policy_text = match fs::read_to_string(path) { + Ok(text) => format!("package main\n\n{}", text), + Err(e) => { + panic!("Failed to read policy file {}: {}", path.display(), e); + } + }; + policy_text +}); + +pub static REGORUS_ENGINE: LazyLock = LazyLock::new(|| { + let mut e = regorus::Engine::new(); + e.add_policy("policy.rego".to_string(), POLICY_TEXT.to_string()) + .expect("can't add policy"); + e +}); diff --git a/foundations/hulypulse/tests/rest_api.rs b/foundations/hulypulse/tests/rest_api.rs new file mode 100644 index 0000000000..6a9f86fa70 --- /dev/null +++ b/foundations/hulypulse/tests/rest_api.rs @@ -0,0 +1,330 @@ +use core::panic; +use reqwest::StatusCode; +use serde_json::Value; +use std::env; + +#[derive(Debug)] +struct ApiResponse { + code: u16, + #[allow(dead_code)] + headers: reqwest::header::HeaderMap, + etag: String, + text: String, + json: Value, +} + +fn server_url() -> String { + env::var("TEST_SERVER_URL").unwrap_or_else(|_| "http://127.0.0.1/api".to_string()) +} + +#[allow(dead_code)] +fn etag(data: &str) -> String { + format!("{:x}", md5::compute(data)) +} + +#[allow(dead_code)] +async fn status(base: &str, client: &reqwest::Client) -> () { + let status_url = format!("{}/status", &base); + let resp = client.get(&status_url).send().await.unwrap(); + + assert_eq!(resp.status(), StatusCode::OK); + let text = resp.text().await.unwrap(); + let json: Value = serde_json::from_str(&text).unwrap(); + + assert_eq!(json["backend"], "memory"); + assert_eq!(json["status"], "OK"); + assert!(json.get("memory_info").is_some()); + assert!(json.get("websockets").is_some()); + // println!("Status: {}", text); +} + +#[allow(dead_code)] +async fn get_not(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () { + let r = engine("get", base, client, workspace, key, "", &[]).await; + assert!(r.code == 404); +} +#[allow(dead_code)] +async fn get(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () { + let r = engine("get", base, client, workspace, key, "", &[]).await; + assert!(r.code == 200); +} +#[allow(dead_code)] +async fn get_data(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> String { + let r = engine("get", base, client, workspace, key, "", &[]).await; + assert!(r.code == 200); + r.json["data"].as_str().unwrap_or("").to_string() +} +#[allow(dead_code)] +async fn get_ttl(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> u64 { + let r = engine("get", base, client, workspace, key, "", &[]).await; + assert!(r.code == 200); + r.json["expires_at"].as_u64().unwrap_or(0) +} +#[allow(dead_code)] +async fn get_etag(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> String { + let r = engine("get", base, client, workspace, key, "", &[]).await; + assert!(r.code == 200); + r.etag +} +#[allow(dead_code)] +async fn put( + base: &str, + client: &reqwest::Client, + workspace: &str, + key: &str, + data: &str, + headers: &[&str], +) -> () { + let r = engine("put", base, client, workspace, key, data, headers).await; + // assert!(r.code == 412); + assert!(r.code == 200); + assert!(r.text == "DONE"); +} + +async fn engine( + method: &str, + base: &str, + client: &reqwest::Client, + workspace: &str, + key: &str, + data: &str, + headers: &[&str], +) -> ApiResponse { + let url = format!("{}/api/{}/{}", base, workspace, key); + let req = client; + let mut req = match method { + "get" => req.get(&url), + "put" => req.put(&url).body(data.to_string()), + "delete" => req.delete(&url), + "list" => req.get(&url), + _ => panic!("Unknown method: {}", method), + }; + + if method == "put" { + req = req.body(data.to_string()); + } + + for h in headers { + if let Some((mut k, v)) = h.split_once(':') { + k = k.trim(); + if k != "IF-MATCH" && k != "IF-NON-MATCH" && k != "HULY-TTL" { + panic!("Only `IF-MATCH`, `IF-NON-MATCH`, `HULY-TTL` headers allowed"); + } + req = req.header(k, v.trim()); + } else { + panic!("Unknown format for header: {}", h); + } + } + + let resp = req.send().await.unwrap(); + + let code = resp.status(); + let headers = resp.headers().clone(); + let text = resp.text().await.unwrap(); + ApiResponse { + code: code.as_u16(), + etag: headers + .get("ETag") + .and_then(|h| h.to_str().ok()) + .unwrap_or("") + .to_string(), + text: text.clone(), + json: serde_json::from_str(&text).unwrap_or_default(), + headers: headers.clone(), + } +} + +#[allow(dead_code)] +async fn delete_any(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () { + engine("delete", base, client, workspace, key, "", &[]).await; +} +#[allow(dead_code)] +async fn delete( + base: &str, + client: &reqwest::Client, + workspace: &str, + key: &str, + headers: &[&str], +) -> () { + let r = engine("delete", base, client, workspace, key, "", headers).await; + assert_eq!(r.code, StatusCode::NO_CONTENT); // 204 + assert!(r.text.is_empty()); +} +#[allow(dead_code)] +async fn delete_not( + base: &str, + client: &reqwest::Client, + workspace: &str, + key: &str, + headers: &[&str], +) -> () { + let r = engine("delete", base, client, workspace, key, "", headers).await; + assert!( + (r.code == StatusCode::NOT_FOUND && r.text == "not found") + || (!headers.is_empty() + && r.code == StatusCode::PRECONDITION_FAILED + && (r.text.contains("412 md5 mismatch") + || r.text.contains("404 Equal: key does not exist"))) + ); + // println!("CODE: {}", r.code); + // println!("TEXT: {}", r.text); +} + +#[tokio::test] +async fn put_and_get() { + let base = server_url(); + // println!("Use url: {}", &base); + + let client = reqwest::Client::builder() + .danger_accept_invalid_certs(true) // только на билдере! + .build() + .unwrap(); + + // Check status + status(&base, &client).await; + + let workspace = "00000000-0000-0000-0000-000000000001"; + let key = "TESTS/key1"; + let data = "Value_1"; + + // test put/get/delete + delete_any(&base, &client, workspace, key).await; + get_not(&base, &client, workspace, key).await; + put(&base, &client, workspace, key, data, &[]).await; + get(&base, &client, workspace, key).await; + delete(&base, &client, workspace, key, &[]).await; + delete_not(&base, &client, workspace, key, &[]).await; + + // test If-Match + delete_any(&base, &client, workspace, key).await; + // delete only tag + put(&base, &client, workspace, key, data, &["HULY-TTL: 2"]).await; + delete_not( + &base, + &client, + workspace, + key, + &["IF-MATCH: 11111111111111111111111111111111"], + ) + .await; + get(&base, &client, workspace, key).await; + delete( + &base, + &client, + workspace, + key, + &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], + ) + .await; + get_not(&base, &client, workspace, key).await; + + // update with tag + put(&base, &client, workspace, key, data, &["HULY-TTL: 2"]).await; + // replace with match + put( + &base, + &client, + workspace, + key, + "Another Data", + &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], + ) + .await; + assert!(get_data(&base, &client, workspace, key).await == "Another Data".to_string()); + // replace with wrong mismatch + let r = engine( + "put", + &base, + &client, + &workspace, + &key, + "Next Another Data", + &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], + ) + .await; + assert!(r.code == StatusCode::PRECONDITION_FAILED && r.text.contains("412 md5 mismatch")); + // match: * + assert!(get_data(&base, &client, workspace, key).await == "Another Data".to_string()); + put( + &base, + &client, + workspace, + key, + "Another Data 2", + &["IF-MATCH: *"], + ) + .await; + assert!(get_data(&base, &client, workspace, key).await == "Another Data 2".to_string()); + // unknown key matched + let fake_key = format!("{}{}", &key, "xxx"); + let r = engine( + "put", + &base, + &client, + &workspace, + &fake_key, + "Next Another Data", + &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], + ) + .await; + assert!(r.code == StatusCode::NOT_FOUND && r.text.contains("404 Equal: key does not exist")); + // if not match + let data3 = "Another Data 3"; + put( + &base, + &client, + &workspace, + &key, + &data3, + &["IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], + ) + .await; + assert!(get_data(&base, &client, &workspace, &key).await == data3.to_string()); + + // DELETE if match + put(&base, &client, &workspace, &key, &data, &[]).await; + delete( + &base, + &client, + &workspace, + &key, + &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], + ) + .await; + + put(&base, &client, &workspace, &key, &data, &[]).await; + delete(&base, &client, &workspace, &key, &["IF-MATCH: *"]).await; + get_not(&base, &client, &workspace, &key).await; + + put(&base, &client, &workspace, &key, &data, &[]).await; + delete(&base, &client, &workspace, &key, &["IF-NON-MATCH: *"]).await; // TODO !!! + get_not(&base, &client, &workspace, &key).await; + + put(&base, &client, &workspace, &key, &data, &[]).await; + let tag = get_etag(&base, &client, &workspace, &key).await; + assert!(tag == "c7bcabf6b98a220f2f4888a18d01568d".to_string()); + delete( + &base, + &client, + &workspace, + &key, + &["IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], + ) + .await; + get_not(&base, &client, &workspace, &key).await; + + // let r = engine("put", &base, &client, &workspace, &key, "Next Another Data", &["HULY-TTL: 2","IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"]).await; + // println!("ALL: {:?}", r); + + // test TTL + delete_any(&base, &client, workspace, key).await; + put(&base, &client, workspace, key, data, &["HULY-TTL: 7"]).await; + let ttl = get_ttl(&base, &client, workspace, key).await; + assert!(ttl == 7 || ttl == 6, "TTL should be 7 (ok, may by 6)"); + put(&base, &client, workspace, key, data, &["HULY-TTL: 1"]).await; + let ttl = get_ttl(&base, &client, workspace, key).await; + assert!(ttl == 1 || ttl == 0, "TTL should be 1 (ok, may by 0)"); + // wait for 1.05 seconds + tokio::time::sleep(tokio::time::Duration::from_millis(1050)).await; + get_not(&base, &client, workspace, key).await; +} diff --git a/foundations/hulypulse/tests/ws.rs b/foundations/hulypulse/tests/ws.rs new file mode 100644 index 0000000000..68f85595a2 --- /dev/null +++ b/foundations/hulypulse/tests/ws.rs @@ -0,0 +1,57 @@ +use futures_util::{SinkExt, StreamExt}; +use serde_json::{Value, json}; +use tokio_tungstenite::{WebSocketStream, connect_async, tungstenite::Message}; + +#[tokio::test] +async fn websocket_echo_id() { + let workspace = "00000000-0000-0000-0000-000000000001"; + let key = "TESTS/key1"; + + let base = + std::env::var("TEST_SERVER_URL").unwrap_or_else(|_| "ws://127.0.0.1:8099/ws".to_string()); + + let (ws_stream, _) = connect_async(&base).await.expect("Can't connect WebSocket"); + let (mut write, mut read) = ws_stream.split(); + + let mut req_id = 1; + + let msg = json!({ + "correlation": req_id.to_string(), + "type": "info" + }); + let r = send(&mut write, &mut read, &msg).await; + assert_eq!(r["correlation"], req_id.to_string(), "ID should match"); + + req_id += 1; + let msg = json!({ + "correlation": req_id.to_string(), + "type": "sub", + "workspace": workspace, + "key": key + }); + let r = send(&mut write, &mut read, &msg).await; + // println!("Answer: {:?}", r); + assert_eq!(r["correlation"], req_id.to_string(), "ID should match"); + assert_eq!(r["result"], "OK", "Subscription should be ok"); +} + +async fn send( + write: &mut futures_util::stream::SplitSink, Message>, + read: &mut futures_util::stream::SplitStream>, + data: &Value, +) -> Value +where + S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin, +{ + // Sending + let msg = Message::Text(data.to_string()); + write.send(msg).await.unwrap(); + + // Waiting for response + while let Some(Ok(Message::Text(resp))) = read.next().await { + let json_resp: Value = serde_json::from_str(&resp).unwrap(); + return json_resp; + } + + panic!("No answer from server"); +} diff --git a/foundations/net/.gitattributes b/foundations/net/.gitattributes new file mode 100644 index 0000000000..79a85db5c2 --- /dev/null +++ b/foundations/net/.gitattributes @@ -0,0 +1,14 @@ +# Don't allow people to merge changes to these generated files, because the result +# may be invalid. You need to run "rush update" again. +pnpm-lock.yaml merge=text +shrinkwrap.yaml merge=binary +npm-shrinkwrap.json merge=binary +yarn.lock merge=binary + +# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic +# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor +# may also require a special configuration to allow comments in JSON. +# +# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 +# +*.json linguist-language=JSON-with-Comments diff --git a/foundations/net/.github/workflows/ci.yml b/foundations/net/.github/workflows/ci.yml new file mode 100644 index 0000000000..ce9256364c --- /dev/null +++ b/foundations/net/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI + +on: + push: + branches: ['main'] + tags: + - 'v0.7.*' + - 's0.7.*' + pull_request: + branches: ['main'] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + - uses: actions/setup-node@v3 + with: + node-version: 22 + - name: Verify Change Logs + run: node common/scripts/install-run-rush.js change --verify + - name: Rush Install + run: node common/scripts/install-run-rush.js install + - name: Rush validate + run: node common/scripts/install-run-rush.js validate --verbose + + publish: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v0.7.') || startsWith(github.ref, 'refs/tags/s0.7.') + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + - uses: actions/setup-node@v3 + with: + node-version: 22 + - name: Install dependencies + run: node common/scripts/install-run-rush.js install + - name: Rush validate + run: node common/scripts/install-run-rush.js validate --verbose + - name: Publish packages + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: node common/scripts/install-run-rush.js publish --include-all --publish + + docker: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Configure docker + uses: docker/setup-docker-action@v4 + with: + daemon-config: | + { + "features": { + "containerd-snapshotter": true + } + } + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + - uses: actions/setup-node@v3 + with: + node-version: 22 + - name: Verify Change Logs + run: node common/scripts/install-run-rush.js change --verify + - name: Rush Install + run: node common/scripts/install-run-rush.js install + - name: Rush validate + run: node common/scripts/install-run-rush.js validate --verbose + + - name: Docker build + run: | + node common/scripts/install-run-rush.js docker:build -v + docker builder prune -a -f + env: + DOCKER_CLI_HINTS: false + DOCKER_EXTRA: --platform=linux/amd64,linux/arm64 + - name: Login to Docker Hub + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/s') }} + uses: docker/login-action@v3 + with: + username: hardcoreeng + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + - name: Docker push tag + if: ${{ startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/s') }} + run: | + echo Pushing release of tag ${{ github.ref }} + node common/scripts/install-run-rush.js docker:push -v diff --git a/foundations/net/.gitignore b/foundations/net/.gitignore new file mode 100644 index 0000000000..a175778200 --- /dev/null +++ b/foundations/net/.gitignore @@ -0,0 +1,115 @@ +.heft/ +lib/ +_api-extractor-temp/ +temp/ +.idea +pods/workspace/init/ +pods/workspace/init-scripts/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +*./rush-logs +*tests/sanity/screenshots + +# Runtime data +*.pid +*.seed +*.pid.lock + +# VS Code settings +.vscode/settings.json + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +# .env + +# next.js build output +.next + +# OS X temporary files +.DS_Store + +# Rush temporary files +common/deploy/ +common/temp/ +common/autoinstallers/*/.npmrc +**/.rush/temp/ +bundle.js +bundle/*.js +dist +.build +typings +types +.validate +tsconfig.tsbuildinfo +ingest-attachment-*.zip +tsdoc-metadata.json +pods/front/dist +*.cpuprofile +*.pyc +metrics.txt +dev/tool/report*.csv +tests/db_dump +.build +.format +tools/apm/apm.js +deploy +metrics.txt +services/github/pod-github/src/github.graphql +.build +.format +dev/tool/report.csv +bundle/* +bundle.js.map +tests/profiles +**/bundle/model.json +.wrangler +dump +**/logs/** +dev/tool/history.json +.aider* +/combined_dependencies +.tmp +ws-tests/docker-compose.override.yml diff --git a/foundations/net/.prettierrc b/foundations/net/.prettierrc new file mode 100644 index 0000000000..00d5897747 --- /dev/null +++ b/foundations/net/.prettierrc @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "trailingComma": "none", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 120, + "useTabs": false, + "bracketSpacing": true, + "proseWrap": "preserve", + "plugins": [], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/foundations/net/.vscode/extensions.json b/foundations/net/.vscode/extensions.json new file mode 100644 index 0000000000..b8552f9c56 --- /dev/null +++ b/foundations/net/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "svelte.svelte-vscode", + "esbenp.prettier-vscode", + "firsttris.vscode-jest-runner" + ] +} diff --git a/foundations/net/.vscode/launch.json b/foundations/net/.vscode/launch.json new file mode 100644 index 0000000000..aae12dfc9d --- /dev/null +++ b/foundations/net/.vscode/launch.json @@ -0,0 +1,51 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "address": "127.0.0.1", + "localRoot": "${workspaceFolder}", + "name": "Attach to Remote", + "port": 9229, + "request": "attach", + "sourceMaps": true, + "skipFiles": ["/**"], + "type": "node" + }, + { + "name": "Debug Network", + "type": "node", + "request": "launch", + "args": ["src/index.ts"], + "env": { + "PORT": "3737", + "DEVELOPMENT": "true" + }, + "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], + "runtimeVersion": "22", + "showAsyncStacks": true, + "outputCapture": "std", + "sourceMaps": true, + "cwd": "${workspaceRoot}/pods/network-pod", + "protocol": "inspector" + }, + { + "name": "Debug Tool", + "type": "node", + "request": "launch", + "args": ["src/index.ts", "bench-agent"], + "env": { + "NETWORK_HOST": "localhost:37371" + }, + "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], + "runtimeVersion": "22", + "showAsyncStacks": true, + "outputCapture": "std", + "sourceMaps": true, + "cwd": "${workspaceRoot}/pods/network-tool", + "protocol": "inspector" + } + ] +} diff --git a/foundations/net/CHANGELOG.md b/foundations/net/CHANGELOG.md new file mode 100644 index 0000000000..ed1f0cc7aa --- /dev/null +++ b/foundations/net/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Comprehensive documentation and examples +- GitHub community health files (CONTRIBUTING.md, SECURITY.md) +- Issue and PR templates + +## [0.7.9] - 2025-10-01 + +### Added + +- Initial public release +- Core network implementation with distributed architecture +- ZeroMQ-based RPC communication layer +- Client libraries for network interaction +- Server implementation with multi-client support +- High availability support with stateless containers +- Automatic failover and health monitoring +- Multi-tenant container management +- Comprehensive test suite +- Docker deployment support +- Full documentation and examples + +### Features + +- Distributed load balancing across multiple agents +- Container lifecycle management with reference counting +- Event broadcasting capabilities +- Request/response communication patterns +- Automatic reconnection and retry logic +- Configurable timeouts for different environments +- Label-based container discovery +- Orphaned container detection and cleanup + +[Unreleased]: https://github.com/hcengineering/huly.net/compare/v0.7.9...HEAD +[0.7.9]: https://github.com/hcengineering/huly.net/releases/tag/v0.7.9 diff --git a/foundations/net/CONTRIBUTING.md b/foundations/net/CONTRIBUTING.md new file mode 100644 index 0000000000..737e169e6f --- /dev/null +++ b/foundations/net/CONTRIBUTING.md @@ -0,0 +1,325 @@ +# Contributing to Huly Virtual Network + +First off, thank you for considering contributing to Huly Virtual Network! It's people like you that make this project such a great tool. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [How Can I Contribute?](#how-can-i-contribute) +- [Development Setup](#development-setup) +- [Pull Request Process](#pull-request-process) +- [Coding Standards](#coding-standards) +- [Testing Guidelines](#testing-guidelines) +- [Commit Message Guidelines](#commit-message-guidelines) + +## Code of Conduct + +This project and everyone participating in it is governed by our commitment to providing a welcoming and inspiring community for all. Please be respectful and constructive in your interactions. + +## How Can I Contribute? + +### Reporting Bugs + +Before creating bug reports, please check the existing issues to avoid duplicates. When you create a bug report, include as many details as possible: + +- **Use a clear and descriptive title** +- **Describe the exact steps to reproduce the problem** +- **Provide specific examples** (code snippets, test cases) +- **Describe the behavior you observed** and what you expected +- **Include logs and error messages** +- **Specify your environment** (Node.js version, OS, etc.) + +### Suggesting Enhancements + +Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion: + +- **Use a clear and descriptive title** +- **Provide a detailed description** of the suggested enhancement +- **Explain why this enhancement would be useful** +- **List any alternatives you've considered** + +### Pull Requests + +We actively welcome your pull requests: + +1. Fork the repo and create your branch from `main` +2. If you've added code that should be tested, add tests +3. If you've changed APIs, update the documentation +4. Ensure the test suite passes +5. Make sure your code follows the existing style +6. Issue your pull request! + +## Development Setup + +### Prerequisites + +- **Node.js**: 22.0.0 or higher +- **PNPM**: 10.15.1 or higher (installed automatically via Rush) +- **ZeroMQ**: Native dependencies (libzmq) + +### Initial Setup + +```bash +# Clone your fork +git clone https://github.com/YOUR_USERNAME/huly.net.git +cd huly.net + +# Install dependencies +node common/scripts/install-run-rush.js install + +# Build all packages +node common/scripts/install-run-rush.js build +``` + +### Project Structure + +``` +huly.net/ +├── packages/ +│ ├── core/ # Core network implementation +│ ├── backrpc/ # ZeroMQ RPC layer +│ ├── client/ # Client libraries +│ └── server/ # Server implementation +├── pods/ +│ └── network-pod/ # Docker deployment +├── tests/ # Integration tests +├── examples/ # Example code +└── docs/ # Documentation +``` + +### Development Workflow + +```bash +# Run tests +node common/scripts/install-run-rush.js test + +# Run tests for a specific package +cd packages/core && npm test + +# Build with watch mode (during development) +node common/scripts/install-run-rush.js build:watch + +# Format code +node common/scripts/install-run-rush.js format + +# Validate TypeScript +node common/scripts/install-run-rush.js validate +``` + +## Pull Request Process + +1. **Update Documentation**: Ensure any new features or changes are documented +2. **Add Tests**: Include tests for new functionality +3. **Update CHANGELOG**: Add your changes to the appropriate package CHANGELOG.md +4. **Pass CI**: Ensure all tests pass in CI +5. **Request Review**: Tag relevant maintainers for review +6. **Sign Commits**: Use `git commit -s` to sign off on your commits + +### PR Title Format + +Use descriptive PR titles that follow this format: + +``` +[Package] Brief description of changes + +Examples: +[core] Add support for custom container timeouts +[client] Fix reconnection logic for dropped connections +[docs] Update production deployment guide +``` + +## Coding Standards + +### TypeScript Style + +- **Use TypeScript strict mode**: All code must pass strict type checking +- **Prefer interfaces over types** for object shapes +- **Use async/await** over raw Promises +- **Document public APIs** with JSDoc comments +- **Use descriptive variable names**: No single-letter variables except in loops + +### Code Organization + +```typescript +// 1. Imports (grouped: external, internal, types) +import { EventEmitter } from 'events' +import { NetworkImpl } from '../network' +import type { Container, ContainerUuid } from '../types' + +// 2. Types and interfaces +interface MyOptions { + timeout: number +} + +// 3. Class implementation +export class MyClass { + // Private fields first + private readonly config: MyOptions + + // Constructor + constructor(options: MyOptions) { + this.config = options + } + + // Public methods + async doSomething(): Promise { + // Implementation + } + + // Private methods + private helper(): void { + // Implementation + } +} +``` + +### Error Handling + +- **Always handle errors explicitly**: No silent failures +- **Use typed errors**: Create custom error classes when needed +- **Provide context**: Include relevant information in error messages + +```typescript +// Good +try { + await operation() +} catch (error: any) { + throw new Error(`Failed to perform operation: ${error.message}`) +} + +// Bad +try { + await operation() +} catch (error) { + // Silent failure +} +``` + +## Testing Guidelines + +### Test Structure + +```typescript +describe('ComponentName', () => { + describe('methodName', () => { + it('should behave correctly under normal conditions', async () => { + // Arrange + const component = new ComponentName() + + // Act + const result = await component.methodName() + + // Assert + expect(result).toBe(expected) + }) + + it('should handle error conditions', async () => { + // Test error cases + }) + }) +}) +``` + +### Test Coverage + +- **Aim for 80%+ coverage**: All new code should have tests +- **Test edge cases**: Don't just test the happy path +- **Test error conditions**: Verify error handling works correctly +- **Integration tests**: Add tests that verify component interaction + +### Running Tests + +```bash +# Run all tests +node common/scripts/install-run-rush.js test + +# Run tests for a specific package +cd packages/core +npm test + +# Run tests in watch mode +npm test -- --watch + +# Generate coverage report +npm test -- --coverage +``` + +## Commit Message Guidelines + +We follow conventional commits for clear git history: + +### Format + +``` +(): + + + +