diff --git a/.vscode/launch.json b/.vscode/launch.json index f5ac5f890a..4f5cd94c89 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -44,6 +44,7 @@ "MINIO_SECRET_KEY": "minioadmin", "SERVER_SECRET": "secret", "COLLABORATOR_URL": "ws://localhost:3078", + "COLLABORATOR_API_URL": "http://localhost:3078", "REKONI_URL": "http://localhost:4004", "FRONT_URL": "http://localhost:8080", "ACCOUNTS_URL": "http://localhost:3000", diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index df885431b7..399365e766 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -13,13 +13,13 @@ dependencies: version: 7.17.13 '@hocuspocus/provider': specifier: ^2.9.0 - version: 2.11.1(bufferutil@4.0.8)(yjs@13.6.12) + version: 2.11.2(bufferutil@4.0.8)(yjs@13.6.12) '@hocuspocus/server': specifier: ^2.9.0 - version: 2.11.1(bufferutil@4.0.8)(yjs@13.6.12) + version: 2.11.2(bufferutil@4.0.8)(yjs@13.6.12) '@hocuspocus/transformer': specifier: ^2.9.0 - version: 2.11.1(@tiptap/pm@2.2.3)(y-prosemirror@1.2.2)(yjs@13.6.12) + version: 2.11.2(@tiptap/pm@2.2.3)(y-prosemirror@1.2.2)(yjs@13.6.12) '@koa/cors': specifier: ^3.1.0 version: 3.4.3 @@ -95,6 +95,9 @@ dependencies: '@rush-temp/client-resources': specifier: file:./projects/client-resources.tgz version: file:projects/client-resources.tgz(@types/node@20.11.19)(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2) + '@rush-temp/collaboration': + specifier: file:./projects/collaboration.tgz + version: file:projects/collaboration.tgz(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2) '@rush-temp/collaborator': specifier: file:./projects/collaborator.tgz version: file:projects/collaborator.tgz(@tiptap/pm@2.2.3)(bufferutil@4.0.8)(prosemirror-model@1.19.4)(svelte@4.2.11) @@ -269,6 +272,9 @@ dependencies: '@rush-temp/model-server-chunter': specifier: file:./projects/model-server-chunter.tgz version: file:projects/model-server-chunter.tgz(svelte@4.2.11) + '@rush-temp/model-server-collaboration': + specifier: file:./projects/model-server-collaboration.tgz + version: file:projects/model-server-collaboration.tgz(svelte@4.2.11)(typescript@5.3.3) '@rush-temp/model-server-contact': specifier: file:./projects/model-server-contact.tgz version: file:projects/model-server-contact.tgz(svelte@4.2.11) @@ -403,7 +409,7 @@ dependencies: version: file:projects/presentation.tgz(@types/node@20.11.19)(esbuild@0.20.0)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2) '@rush-temp/prod': specifier: file:./projects/prod.tgz - version: file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.70.0)(ts-node@10.9.2) + version: file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.71.0)(ts-node@10.9.2) '@rush-temp/query': specifier: file:./projects/query.tgz version: file:projects/query.tgz(@types/node@20.11.19)(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2) @@ -461,6 +467,12 @@ dependencies: '@rush-temp/server-chunter-resources': specifier: file:./projects/server-chunter-resources.tgz version: file:projects/server-chunter-resources.tgz(@types/node@20.11.19)(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2) + '@rush-temp/server-collaboration': + specifier: file:./projects/server-collaboration.tgz + version: file:projects/server-collaboration.tgz(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2) + '@rush-temp/server-collaboration-resources': + specifier: file:./projects/server-collaboration-resources.tgz + version: file:projects/server-collaboration-resources.tgz(@types/node@20.11.19)(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2) '@rush-temp/server-contact': specifier: file:./projects/server-contact.tgz version: file:projects/server-contact.tgz(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2) @@ -679,7 +691,7 @@ dependencies: version: 7.6.16(react@18.2.0) '@storybook/addon-styling': specifier: ^1.0.1 - version: 1.3.7(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0)(sass@1.70.0)(typescript@5.3.3)(webpack@5.90.2) + version: 1.3.7(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)(typescript@5.3.3)(webpack@5.90.2) '@storybook/blocks': specifier: ^7.0.6 version: 7.6.16(react-dom@18.2.0)(react@18.2.0) @@ -865,13 +877,16 @@ dependencies: version: 6.21.0(eslint@8.56.0)(typescript@5.3.3) allure-playwright: specifier: ^2.9.2 - version: 2.12.1 + version: 2.12.2 autolinker: specifier: 4.0.0 version: 4.0.0 autoprefixer: specifier: ^10.4.14 version: 10.4.17(postcss@8.4.35) + base64-js: + specifier: ^1.5.1 + version: 1.5.1 body-parser: specifier: ~1.19.1 version: 1.19.2 @@ -1027,7 +1042,7 @@ dependencies: version: 1.0.5 lib0: specifier: ^0.2.88 - version: 0.2.88 + version: 0.2.89 libphonenumber-js: specifier: ^1.9.46 version: 1.10.56 @@ -1099,10 +1114,10 @@ dependencies: version: 5.1.1 sass: specifier: ^1.53.0 - version: 1.70.0 + version: 1.71.0 sass-loader: specifier: ^13.2.0 - version: 13.3.3(sass@1.70.0)(webpack@5.90.2) + version: 13.3.3(sass@1.71.0)(webpack@5.90.2) sharp: specifier: ~0.32.0 version: 0.32.6 @@ -1129,7 +1144,7 @@ dependencies: version: 4.2.11 svelte-check: specifier: ^3.6.0 - version: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + version: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: specifier: ^0.33.1 version: 0.33.1(svelte@4.2.11) @@ -1138,7 +1153,7 @@ dependencies: version: 3.1.9(svelte@4.2.11) svelte-preprocess: specifier: ^5.1.0 - version: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + version: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) svgo-loader: specifier: ^3.0.0 version: 3.0.3 @@ -3276,21 +3291,21 @@ packages: tslib: 2.6.2 dev: false - /@hocuspocus/common@2.11.1: - resolution: {integrity: sha512-IpDrdIwaPAlVWnAr8Oq1RbsCO2PZjqR5z5HghPIXv/xZjj1X6bSMrWV4eZ6w2D82H9yMwg+W+6rIW/i7QIjdYg==} + /@hocuspocus/common@2.11.2: + resolution: {integrity: sha512-Ej3ehUYweYrlVArYdKe3grvEDO2wbjpy/IoZrtklHI4ydChLkXJCT6nTelM69dOtDG8IfX9WPIXiwwJF7CGNcA==} dependencies: - lib0: 0.2.88 + lib0: 0.2.89 dev: false - /@hocuspocus/provider@2.11.1(bufferutil@4.0.8)(yjs@13.6.12): - resolution: {integrity: sha512-lV5Sw75pCVkfdnaFLLpp5YzpAYTbVPuTZaxgdJOI2NmH+PpAn7AshQ3eAn5S/2XERl2E9gJtOa3z13bfvH224Q==} + /@hocuspocus/provider@2.11.2(bufferutil@4.0.8)(yjs@13.6.12): + resolution: {integrity: sha512-h6/2KZC8Z3Wz+rP17WQ5eqxdgtsq5JZ4iC72WSXdIOJn3RO4sv63ZvT+EWc4aA1YM7UVDBYVjdOjMtEYZF0Flg==} peerDependencies: y-protocols: ^1.0.6 yjs: ^13.6.8 dependencies: - '@hocuspocus/common': 2.11.1 + '@hocuspocus/common': 2.11.2 '@lifeomic/attempt': 3.0.3 - lib0: 0.2.88 + lib0: 0.2.89 ws: 8.16.0(bufferutil@4.0.8) yjs: 13.6.12 transitivePeerDependencies: @@ -3298,16 +3313,16 @@ packages: - utf-8-validate dev: false - /@hocuspocus/server@2.11.1(bufferutil@4.0.8)(yjs@13.6.12): - resolution: {integrity: sha512-VMEoYLwd+dBBD2trw6AjyzIWPbeAju6SsSzb2sXuIs6E9N1RTyfwS02legM6rlMQcJf5dfy2cLQS67htr/rk6w==} + /@hocuspocus/server@2.11.2(bufferutil@4.0.8)(yjs@13.6.12): + resolution: {integrity: sha512-/djaNUSS9vYmz5H/bformB9BXKVhSXS10WIURT1OqFLnN0dZnOB0gxL/IyGCZgXCNyiD8U03n7FIgad9MckV8g==} peerDependencies: y-protocols: ^1.0.6 yjs: ^13.6.8 dependencies: - '@hocuspocus/common': 2.11.1 + '@hocuspocus/common': 2.11.2 async-lock: 1.4.1 kleur: 4.1.5 - lib0: 0.2.88 + lib0: 0.2.89 uuid: 9.0.1 ws: 8.16.0(bufferutil@4.0.8) yjs: 13.6.12 @@ -3316,8 +3331,8 @@ packages: - utf-8-validate dev: false - /@hocuspocus/transformer@2.11.1(@tiptap/pm@2.2.3)(y-prosemirror@1.2.2)(yjs@13.6.12): - resolution: {integrity: sha512-cr5T4LW+jy4Y4Vj6l7NH166enhEGdZ0epoqaGoU2Xz5qg53hccc0cXctoTUR/fn3b1kU6P6UCcRhpKlAgRtk5w==} + /@hocuspocus/transformer@2.11.2(@tiptap/pm@2.2.3)(y-prosemirror@1.2.2)(yjs@13.6.12): + resolution: {integrity: sha512-xR46lVsxZoODZ33ZvGhrRAw+JTXPbq7IFpQ/DGHvIp9rdijr6buQlDYCZv9A9L1rAqDhwRtsMbwFj7jOpzKHHA==} peerDependencies: '@tiptap/pm': ^2.1.12 y-prosemirror: ^1.2.1 @@ -3687,7 +3702,7 @@ packages: react: '>=16' dependencies: '@types/mdx': 2.0.11 - '@types/react': 18.2.55 + '@types/react': 18.2.56 react: 18.2.0 dev: false @@ -4513,7 +4528,7 @@ packages: ts-dedent: 2.2.0 dev: false - /@storybook/addon-styling@1.3.7(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0)(sass@1.70.0)(typescript@5.3.3)(webpack@5.90.2): + /@storybook/addon-styling@1.3.7(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)(typescript@5.3.3)(webpack@5.90.2): resolution: {integrity: sha512-JSBZMOrSw/3rlq5YoEI7Qyq703KSNP0Jd+gxTWu3/tP6245mpjn2dXnR8FvqVxCi+FG4lt2kQyPzgsuwEw1SSA==} hasBin: true peerDependencies: @@ -4553,7 +4568,7 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) resolve-url-loader: 5.0.0 - sass-loader: 13.3.3(sass@1.70.0)(webpack@5.90.2) + sass-loader: 13.3.3(sass@1.71.0)(webpack@5.90.2) style-loader: 3.3.4(webpack@5.90.2) webpack: 5.90.2(esbuild@0.20.0)(webpack-cli@5.1.4) transitivePeerDependencies: @@ -4621,7 +4636,7 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) regenerator-runtime: 0.13.11 - store2: 2.14.2 + store2: 2.14.3 telejson: 6.0.8 ts-dedent: 2.2.0 util-deprecate: 1.0.2 @@ -4981,7 +4996,7 @@ packages: express: 4.18.2 fs-extra: 11.2.0 globby: 11.1.0 - ip: 2.0.0 + ip: 2.0.1 lodash: 4.17.21 open: 8.4.2 pretty-hrtime: 1.0.3 @@ -5101,7 +5116,7 @@ packages: dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 - store2: 2.14.2 + store2: 2.14.3 telejson: 7.2.0 ts-dedent: 2.2.0 transitivePeerDependencies: @@ -5820,9 +5835,9 @@ packages: prosemirror-schema-list: 1.3.0 prosemirror-state: 1.4.3 prosemirror-tables: 1.3.5 - prosemirror-trailing-node: 2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.32.7) + prosemirror-trailing-node: 2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.33.1) prosemirror-transform: 1.8.0 - prosemirror-view: 1.32.7 + prosemirror-view: 1.33.1 dev: false /@tiptap/starter-kit@2.2.3(@tiptap/pm@2.2.3): @@ -6289,8 +6304,8 @@ packages: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: false - /@types/react@18.2.55: - resolution: {integrity: sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==} + /@types/react@18.2.56: + resolution: {integrity: sha512-NpwHDMkS/EFZF2dONFQHgkPRwhvgq/OAvIaGQzxGSBmaeR++kTg6njr15Vatz0/2VcCEwJQFi6Jf4Q0qBu0rLA==} dependencies: '@types/prop-types': 15.7.11 '@types/scheduler': 0.16.8 @@ -6897,17 +6912,17 @@ packages: uri-js: 4.4.1 dev: false - /allure-js-commons@2.12.1: - resolution: {integrity: sha512-gx8DBwUrXpfoielzTp8bvUIa+IERaO0ZMlu7NioembuHawVOINQSuX63o1TH1S8k7K4A2gHoiHf+v2x4BAmAaQ==} + /allure-js-commons@2.12.2: + resolution: {integrity: sha512-bapkOHuwOYFR62aeNNwFmf8+LZSchzQ4Q8cFXWvEqP5fBTgADA+GujsRl936gjmTmKWVmKYwQfUt+PZw6tgFzw==} dependencies: properties: 1.2.1 strip-ansi: 5.2.0 dev: false - /allure-playwright@2.12.1: - resolution: {integrity: sha512-1JE2j9Vx1TPrwkUNfcgXDdOlb0NTuzWEZ70HJOae6VnostL/sTG5b1ISFPVmSax8G5H10iMQfwO7XjDOYIB4MQ==} + /allure-playwright@2.12.2: + resolution: {integrity: sha512-QvDyCHABYlZ02PyGevbBZc9tS+Li0zXhTaTC1IJ2wJrd8q9cSS3mUnzAJUjPUuCtofdLBwePd2AuzsYE2ZXEJQ==} dependencies: - allure-js-commons: 2.12.1 + allure-js-commons: 2.12.2 dev: false /ansi-colors@4.1.3: @@ -7170,7 +7185,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.23.0 - caniuse-lite: 1.0.30001587 + caniuse-lite: 1.0.30001588 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -7339,7 +7354,7 @@ packages: bare-events: 2.2.0 bare-os: 2.2.0 bare-path: 2.1.0 - streamx: 2.15.8 + streamx: 2.16.0 dev: false optional: true @@ -7519,8 +7534,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001587 - electron-to-chromium: 1.4.671 + caniuse-lite: 1.0.30001588 + electron-to-chromium: 1.4.673 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.21.5) dev: false @@ -7530,8 +7545,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001587 - electron-to-chromium: 1.4.671 + caniuse-lite: 1.0.30001588 + electron-to-chromium: 1.4.673 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) dev: false @@ -7659,8 +7674,8 @@ packages: engines: {node: '>=10'} dev: false - /caniuse-lite@1.0.30001587: - resolution: {integrity: sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==} + /caniuse-lite@1.0.30001588: + resolution: {integrity: sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==} dev: false /case-anything@2.1.13: @@ -8748,8 +8763,8 @@ packages: - supports-color dev: false - /electron-to-chromium@1.4.671: - resolution: {integrity: sha512-UUlE+/rWbydmp+FW8xlnnTA5WNA0ZZd2XL8CuMS72rh+k4y1f8+z6yk3UQhEwqHQWj6IBdL78DwWOdGMvYfQyA==} + /electron-to-chromium@1.4.673: + resolution: {integrity: sha512-zjqzx4N7xGdl5468G+vcgzDhaHkaYgVcf9MqgexcTqsl2UHSCmOj/Bi3HAprg4BZCpC7HyD8a6nZl6QAZf72gw==} dev: false /email-addresses@5.0.0: @@ -8899,7 +8914,7 @@ packages: string.prototype.trimstart: 1.0.7 typed-array-buffer: 1.0.1 typed-array-byte-length: 1.0.0 - typed-array-byte-offset: 1.0.0 + typed-array-byte-offset: 1.0.1 typed-array-length: 1.0.4 unbox-primitive: 1.0.2 which-typed-array: 1.1.14 @@ -10683,8 +10698,8 @@ packages: loose-envify: 1.4.0 dev: false - /ip@2.0.0: - resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} + /ip@2.0.1: + resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} dev: false /ipaddr.js@1.9.1: @@ -11892,8 +11907,8 @@ packages: resolution: {integrity: sha512-K1B/Yr/gIU0wm68hk/yB0p/mv6xM3ShD5aci42vOwcjof8slG8Kpo3Q7+1WTv7DaRHKWRgLPqrFDt+4GtuFAtA==} dev: false - /lib0@0.2.88: - resolution: {integrity: sha512-KyroiEvCeZcZEMx5Ys+b4u4eEBbA1ch7XUaBhYpwa/nPMrzTjUhI4RfcytmQfYoTBPcdyx+FX6WFNIoNuJzJfQ==} + /lib0@0.2.89: + resolution: {integrity: sha512-5j19vcCjsQhvLG6mcDD+nprtJUCbmqLz5Hzt5xgi9SV6RIW/Dty7ZkVZHGBuPOADMKjQuKDvuQTH495wsmw8DQ==} engines: {node: '>=16'} hasBin: true dependencies: @@ -11909,8 +11924,8 @@ packages: engines: {node: '>=10'} dev: false - /lilconfig@3.1.0: - resolution: {integrity: sha512-p3cz0JV5vw/XeouBU3Ldnp+ZkBjE+n8ydJ4mcwBrOiXXPqNlrzGBqWs9X4MWF7f+iKUBu794Y8Hh8yawiJbCjw==} + /lilconfig@3.1.1: + resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} engines: {node: '>=14'} dev: false @@ -13216,7 +13231,7 @@ packages: ts-node: optional: true dependencies: - lilconfig: 3.1.0 + lilconfig: 3.1.1 postcss: 8.4.35 ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) yaml: 2.3.4 @@ -13461,7 +13476,7 @@ packages: dependencies: prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 - prosemirror-view: 1.32.7 + prosemirror-view: 1.33.1 dev: false /prosemirror-gapcursor@1.3.2: @@ -13470,7 +13485,7 @@ packages: prosemirror-keymap: 1.2.2 prosemirror-model: 1.19.4 prosemirror-state: 1.4.3 - prosemirror-view: 1.32.7 + prosemirror-view: 1.33.1 dev: false /prosemirror-history@1.3.2: @@ -13478,7 +13493,7 @@ packages: dependencies: prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 - prosemirror-view: 1.32.7 + prosemirror-view: 1.33.1 rope-sequence: 1.3.4 dev: false @@ -13537,7 +13552,7 @@ packages: dependencies: prosemirror-model: 1.19.4 prosemirror-transform: 1.8.0 - prosemirror-view: 1.32.7 + prosemirror-view: 1.33.1 dev: false /prosemirror-tables@1.3.5: @@ -13547,10 +13562,10 @@ packages: prosemirror-model: 1.19.4 prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 - prosemirror-view: 1.32.7 + prosemirror-view: 1.33.1 dev: false - /prosemirror-trailing-node@2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.32.7): + /prosemirror-trailing-node@2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.33.1): resolution: {integrity: sha512-8zcZORYj/8WEwsGo6yVCRXFMOfBo0Ub3hCUvmoWIZYfMP26WqENU0mpEP27w7mt8buZWuGrydBewr0tOArPb1Q==} peerDependencies: prosemirror-model: ^1.19.0 @@ -13562,7 +13577,7 @@ packages: escape-string-regexp: 4.0.0 prosemirror-model: 1.19.4 prosemirror-state: 1.4.3 - prosemirror-view: 1.32.7 + prosemirror-view: 1.33.1 dev: false /prosemirror-transform@1.8.0: @@ -13571,8 +13586,8 @@ packages: prosemirror-model: 1.19.4 dev: false - /prosemirror-view@1.32.7: - resolution: {integrity: sha512-pvxiOoD4shW41X5bYDjRQk3DSG4fMqxh36yPMt7VYgU3dWRmqFzWJM/R6zeo1KtC8nyk717ZbQND3CC9VNeptw==} + /prosemirror-view@1.33.1: + resolution: {integrity: sha512-62qkYgSJIkwIMMCpuGuPzc52DiK1Iod6TWoIMxP4ja6BTD4yO8kCUL64PZ/WhH/dJ9fW0CDO39FhH1EMyhUFEg==} dependencies: prosemirror-model: 1.19.4 prosemirror-state: 1.4.3 @@ -14204,7 +14219,7 @@ packages: rimraf: 2.7.1 dev: false - /sass-loader@13.3.3(sass@1.70.0)(webpack@5.90.2): + /sass-loader@13.3.3(sass@1.71.0)(webpack@5.90.2): resolution: {integrity: sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==} engines: {node: '>= 14.15.0'} peerDependencies: @@ -14224,12 +14239,12 @@ packages: optional: true dependencies: neo-async: 2.6.2 - sass: 1.70.0 + sass: 1.71.0 webpack: 5.90.2(esbuild@0.20.0)(webpack-cli@5.1.4) dev: false - /sass@1.70.0: - resolution: {integrity: sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==} + /sass@1.71.0: + resolution: {integrity: sha512-HKKIKf49Vkxlrav3F/w6qRuPcmImGVbIXJ2I3Kg0VMA+3Bav+8yE9G5XmP5lMj6nl4OlqbPftGAscNaNu28b8w==} engines: {node: '>=14.0.0'} hasBin: true dependencies: @@ -14664,8 +14679,8 @@ packages: internal-slot: 1.0.7 dev: false - /store2@2.14.2: - resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} + /store2@2.14.3: + resolution: {integrity: sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==} dev: false /storybook-addon-themes@6.1.0(react-dom@18.2.0)(react@18.2.0)(svelte@4.2.11): @@ -14723,8 +14738,8 @@ packages: engines: {node: '>=10.0.0'} dev: false - /streamx@2.15.8: - resolution: {integrity: sha512-6pwMeMY/SuISiRsuS8TeIrAzyFbG5gGPHFQsYjUr/pbBadaL1PCWmzKw+CHZSwainfvcF6Si6cVLq4XTEwswFQ==} + /streamx@2.16.0: + resolution: {integrity: sha512-a7Fi0PoUeusrUcMS4+HxivnZqYsw2MFEP841TIyLxTcEIucHcJsk+0ARcq3tGq1xDn+xK7sKHetvfMzI1/CzMA==} dependencies: fast-fifo: 1.3.2 queue-tick: 1.0.1 @@ -14897,7 +14912,7 @@ packages: engines: {node: '>= 0.4'} dev: false - /svelte-check@3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11): + /svelte-check@3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11): resolution: {integrity: sha512-mY/dqucqm46p72M8yZmn81WPZx9mN6uuw8UVfR3ZKQeLxQg5HDGO3HHm5AZuWZPYNMLJ+TRMn+TeN53HfQ/vsw==} hasBin: true peerDependencies: @@ -14910,7 +14925,7 @@ packages: picocolors: 1.0.0 sade: 1.8.1 svelte: 4.2.11 - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: - '@babel/core' @@ -14965,7 +14980,7 @@ packages: svelte-hmr: 0.14.12(svelte@4.2.11) dev: false - /svelte-preprocess@5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3): + /svelte-preprocess@5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3): resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==} engines: {node: '>= 16.0.0', pnpm: ^8.0.0} requiresBuild: true @@ -15008,7 +15023,7 @@ packages: magic-string: 0.30.7 postcss: 8.4.35 postcss-load-config: 4.0.2(postcss@8.4.35)(ts-node@10.9.2) - sass: 1.70.0 + sass: 1.71.0 sorcery: 0.11.0 strip-indent: 3.0.0 svelte: 4.2.11 @@ -15122,7 +15137,7 @@ packages: dependencies: b4a: 1.6.6 fast-fifo: 1.3.2 - streamx: 2.15.8 + streamx: 2.16.0 dev: false /tar@6.2.0: @@ -15544,13 +15559,14 @@ packages: is-typed-array: 1.1.13 dev: false - /typed-array-byte-offset@1.0.0: - resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + /typed-array-byte-offset@1.0.1: + resolution: {integrity: sha512-tcqKMrTRXjqvHN9S3553NPCaGL0VPgFI92lXszmrE8DMhiDPLBYLlvo8Uu4WZAAX/aGqp/T1sbA4ph8EWjDF9Q==} engines: {node: '>= 0.4'} dependencies: available-typed-arrays: 1.0.6 call-bind: 1.0.7 for-each: 0.3.3 + gopd: 1.0.1 has-proto: 1.0.1 is-typed-array: 1.1.13 dev: false @@ -16347,7 +16363,7 @@ packages: yjs: ^13.0.0 dependencies: level: 6.0.1 - lib0: 0.2.88 + lib0: 0.2.89 yjs: 13.6.12 dev: false optional: true @@ -16362,7 +16378,7 @@ packages: y-protocols: ^1.0.1 yjs: ^13.5.38 dependencies: - lib0: 0.2.88 + lib0: 0.2.89 prosemirror-model: 1.19.4 yjs: 13.6.12 dev: false @@ -16373,7 +16389,7 @@ packages: peerDependencies: yjs: ^13.0.0 dependencies: - lib0: 0.2.88 + lib0: 0.2.89 yjs: 13.6.12 dev: false @@ -16384,7 +16400,7 @@ packages: peerDependencies: yjs: ^13.5.6 dependencies: - lib0: 0.2.88 + lib0: 0.2.89 lodash.debounce: 4.0.8 y-protocols: 1.0.6(yjs@13.6.12) yjs: 13.6.12 @@ -16449,7 +16465,7 @@ packages: resolution: {integrity: sha512-KOT8ILoyVH2f/PxPadeu5kVVS055D1r3x1iFfJVJzFdnN98pVGM8H07NcKsO+fG3F7/0tf30Vnokf5YIqhU/iw==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} dependencies: - lib0: 0.2.88 + lib0: 0.2.89 dev: false /ylru@1.3.2: @@ -16576,12 +16592,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -16753,12 +16769,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -16866,12 +16882,12 @@ packages: prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) qs: 6.11.2 - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -16978,12 +16994,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -17090,12 +17106,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -17200,12 +17216,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -17324,6 +17340,40 @@ packages: - ts-node dev: false + file:projects/collaboration.tgz(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2): + resolution: {integrity: sha512-qXhZcchQGZkBIG7GaLE8aTgiZ3FcaWB4Rssu+J5LnAABCkxTVMIRv5YYe3Qr1o+t4FXay9OxpLyFIlIgQV3kVg==, tarball: file:projects/collaboration.tgz} + id: file:projects/collaboration.tgz + name: '@rush-temp/collaboration' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + base64-js: 1.5.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) + ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + yjs: 13.6.12 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - svelte + - ts-node + dev: false + file:projects/collaborator-client.tgz(svelte@4.2.11)(ts-node@10.9.2): resolution: {integrity: sha512-WWZ6fF9+N9PK5ivTvV0YI0u0gXQ9afedg7omhmUegEXukx9d49HXgd0WggIRSQGhB44fM5n13qNnArB2HokzYw==, tarball: file:projects/collaborator-client.tgz} id: file:projects/collaborator-client.tgz @@ -17358,13 +17408,13 @@ packages: dev: false file:projects/collaborator.tgz(@tiptap/pm@2.2.3)(bufferutil@4.0.8)(prosemirror-model@1.19.4)(svelte@4.2.11): - resolution: {integrity: sha512-iT81A/XmQMVNOeZoG0x1+GeZM/0PwZmz3qxvO5aYCl52NSbMfe25fDmrBEt2oO6In2w/PUfxyq176wfX2S/EqA==, tarball: file:projects/collaborator.tgz} + resolution: {integrity: sha512-HvLgY+NwEKic10k5ZvdykepwdECRL2FQHFp3XfjK3Li7pvHMPzKUcTYfnBsJFSHD+jjEZ31C2wP9oRD5s5lSag==, tarball: file:projects/collaborator.tgz} id: file:projects/collaborator.tgz name: '@rush-temp/collaborator' version: 0.0.0 dependencies: - '@hocuspocus/server': 2.11.1(bufferutil@4.0.8)(yjs@13.6.12) - '@hocuspocus/transformer': 2.11.1(@tiptap/pm@2.2.3)(y-prosemirror@1.2.2)(yjs@13.6.12) + '@hocuspocus/server': 2.11.2(bufferutil@4.0.8)(yjs@13.6.12) + '@hocuspocus/transformer': 2.11.2(@tiptap/pm@2.2.3)(y-prosemirror@1.2.2)(yjs@13.6.12) '@tiptap/core': 2.2.3(@tiptap/pm@2.2.3) '@tiptap/html': 2.2.3(@tiptap/core@2.2.3)(@tiptap/pm@2.2.3) '@types/body-parser': 1.19.5 @@ -17473,12 +17523,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -17719,12 +17769,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -17958,12 +18008,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -18067,12 +18117,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -18145,13 +18195,13 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 smartcrop: 2.0.5 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -18255,12 +18305,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -18333,12 +18383,12 @@ packages: lexorank: 1.0.5 prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -18410,12 +18460,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -18519,12 +18569,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 url-loader: 4.1.1(file-loader@6.2.0)(webpack@5.90.2) @@ -18670,7 +18720,7 @@ packages: dev: false file:projects/model-all.tgz(svelte@4.2.11): - resolution: {integrity: sha512-7ab6irtrhsaaclRaPZXRfJKJNWEAPMm9664ehVmeZEKW4xwU3yPLbQP/tnh84zyl2WiKg3muEA90gPFoXLkjTg==, tarball: file:projects/model-all.tgz} + resolution: {integrity: sha512-oRRJx0V+rtUYxrRRDBQwb0ttCzClI/SVBXS57BFT2Qx1xYZ6fPSjZQWlca0KFztUqbg4qLnoib7Tq26otiPYEw==, tarball: file:projects/model-all.tgz} id: file:projects/model-all.tgz name: '@rush-temp/model-all' version: 0.0.0 @@ -19117,6 +19167,27 @@ packages: - svelte dev: false + file:projects/model-server-collaboration.tgz(svelte@4.2.11)(typescript@5.3.3): + resolution: {integrity: sha512-1QoME1SopcOTJBUK6ef+17zk4Zh82sV8XVAresuAVgtEoRhtZU2v2mKrNPMM7UGaYawzi/J2+E5vX4HHFMXtwg==, tarball: file:projects/model-server-collaboration.tgz} + id: file:projects/model-server-collaboration.tgz + name: '@rush-temp/model-server-collaboration' + version: 0.0.0 + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + prettier: 3.2.5 + prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) + transitivePeerDependencies: + - supports-color + - svelte + - typescript + dev: false + file:projects/model-server-contact.tgz(svelte@4.2.11): resolution: {integrity: sha512-VoPRtOcvPnTkD9vREuWCJnXeBtJB6TKKtZdCm89O6gEwc8TtXSQr0OSvJjqB/YPMn96ltxFqztLdeoD6D82UPQ==, tarball: file:projects/model-server-contact.tgz} id: file:projects/model-server-contact.tgz @@ -19831,12 +19902,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -19876,10 +19947,10 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + sass: 1.71.0 + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -19956,12 +20027,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -20217,7 +20288,7 @@ packages: dev: false file:projects/pod-server.tgz(svelte@4.2.11): - resolution: {integrity: sha512-XWFlJ3y/O7PTHm71WjSoLAV5t2kRAyl3ghPfolvue5PVYz7apYP5Fc9JMV5TLMHY5Zc9dbCJj4k/6jWfQKkNkg==, tarball: file:projects/pod-server.tgz} + resolution: {integrity: sha512-Vu7R/lyLRjCyqDTofzOdVCFk67PxBM5neEHd82HBLrNniUjVrmCfEFt4IBQOgpoTy1cM73swFSsl7UEyLA0VOQ==, tarball: file:projects/pod-server.tgz} id: file:projects/pod-server.tgz name: '@rush-temp/pod-server' version: 0.0.0 @@ -20336,12 +20407,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -20363,8 +20434,8 @@ packages: - ts-node dev: false - file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.70.0)(ts-node@10.9.2): - resolution: {integrity: sha512-TjeUSPaVhCjzTrSNqMLuwcUK6yYoS5OaNhKS8UD9rb21+XjCarJAcZg+KEADKJHqAP29rZAFF8EmrcJDin+bmw==, tarball: file:projects/prod.tgz} + file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.71.0)(ts-node@10.9.2): + resolution: {integrity: sha512-wxoPP5P+qy7KnUurqydjBwLhbA7sDZ2DKpp0IZeHAUM1XVUyGG7+lmtH4D5s3iCVCwzvZ8qZks66ncp3ejqeFQ==, tarball: file:projects/prod.tgz} id: file:projects/prod.tgz name: '@rush-temp/prod' version: 0.0.0 @@ -20385,7 +20456,7 @@ packages: postcss: 8.4.35 postcss-load-config: 4.0.2(postcss@8.4.35)(ts-node@10.9.2) postcss-loader: 7.3.4(postcss@8.4.35)(typescript@5.3.3)(webpack@5.90.2) - sass-loader: 13.3.3(sass@1.70.0)(webpack@5.90.2) + sass-loader: 13.3.3(sass@1.71.0)(webpack@5.90.2) style-loader: 3.3.4(webpack@5.90.2) svelte: 4.2.11 svelte-loader: 3.1.9(svelte@4.2.11) @@ -20498,12 +20569,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -20639,12 +20710,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -20959,7 +21030,7 @@ packages: dev: false file:projects/server-chunter-resources.tgz(@types/node@20.11.19)(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2): - resolution: {integrity: sha512-61cG5pl6YCoR2j56LDucXOxQXzIh6X9xjPBL80R5HrzaG5IjdVPfi/jpRVWulEHySTU5OtsaOJey7ahyWZ8A4g==, tarball: file:projects/server-chunter-resources.tgz} + resolution: {integrity: sha512-MrYaCa3ooPlUq3aTivSYKjKXe+e54sntrUd6kk5Wuad4mXdCVNQsDEkr75wQqJveZy6T6wUBJhrSynoxDUN7mg==, tarball: file:projects/server-chunter-resources.tgz} id: file:projects/server-chunter-resources.tgz name: '@rush-temp/server-chunter-resources' version: 0.0.0 @@ -21022,6 +21093,70 @@ packages: - ts-node dev: false + file:projects/server-collaboration-resources.tgz(@types/node@20.11.19)(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2): + resolution: {integrity: sha512-hUnGZo30ZwQpRw9dKqFWXouUZ9POmp9aYoUTIqbc5C1GxX7YnTU3At6zp4mF9fLuZFpud4AT7UDAgxOxOK2lZQ==, tarball: file:projects/server-collaboration-resources.tgz} + id: file:projects/server-collaboration-resources.tgz + name: '@rush-temp/server-collaboration-resources' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) + ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@types/node' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - svelte + - ts-node + dev: false + + file:projects/server-collaboration.tgz(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2): + resolution: {integrity: sha512-WOn1XPWSBWD7ta+yliKToO9YJa4FQMMNkEKGfihx88IRvYQ3CxNKlGUf8KFjZcKmG8hkqoY5rJeUhY6h2V5OgA==, tarball: file:projects/server-collaboration.tgz} + id: file:projects/server-collaboration.tgz + name: '@rush-temp/server-collaboration' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) + ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - svelte + - ts-node + dev: false + file:projects/server-contact-resources.tgz(@types/node@20.11.19)(esbuild@0.20.0)(svelte@4.2.11)(ts-node@10.9.2): resolution: {integrity: sha512-82ifj50URjlShjKPcCL7LNXbIM8Vb87XcozV4RtjQmkmsfok1xNFRgpFkjO8ddOVMVh5y74oJpnvwEFEc8RUdQ==, tarball: file:projects/server-contact-resources.tgz} id: file:projects/server-contact-resources.tgz @@ -22224,12 +22359,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -22292,7 +22427,7 @@ packages: '@storybook/addon-essentials': 7.6.16(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-interactions': 7.6.16 '@storybook/addon-links': 7.6.16(react@18.2.0) - '@storybook/addon-styling': 1.3.7(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0)(sass@1.70.0)(typescript@5.3.3)(webpack@5.90.2) + '@storybook/addon-styling': 1.3.7(postcss@8.4.35)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)(typescript@5.3.3)(webpack@5.90.2) '@storybook/blocks': 7.6.16(react-dom@18.2.0)(react@18.2.0) '@storybook/svelte': 7.6.16(svelte@4.2.11) '@storybook/svelte-webpack5': 7.6.16(esbuild@0.20.0)(svelte-loader@3.1.9)(svelte@4.2.11)(typescript@5.3.3)(webpack-cli@5.1.4) @@ -22300,11 +22435,11 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) resolve-url-loader: 5.0.0 - sass: 1.70.0 - sass-loader: 13.3.3(sass@1.70.0)(webpack@5.90.2) + sass: 1.71.0 + sass-loader: 13.3.3(sass@1.71.0)(webpack@5.90.2) storybook: 7.6.16(bufferutil@4.0.8) storybook-addon-themes: 6.1.0(react-dom@18.2.0)(react@18.2.0)(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) transitivePeerDependencies: - '@babel/core' - '@rspack/core' @@ -22385,12 +22520,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -22494,12 +22629,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -22603,12 +22738,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -22713,12 +22848,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -22822,12 +22957,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -22892,7 +23027,7 @@ packages: '@types/node': 20.11.19 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) - allure-playwright: 2.12.1 + allure-playwright: 2.12.2 cross-env: 7.0.3 dotenv: 16.0.3 eslint: 8.56.0 @@ -22909,12 +23044,12 @@ packages: dev: false file:projects/text-editor.tgz(@types/node@20.11.19)(bufferutil@4.0.8)(esbuild@0.20.0)(postcss-load-config@4.0.2)(postcss@8.4.35)(prosemirror-model@1.19.4)(ts-node@10.9.2): - resolution: {integrity: sha512-aXtV6nrkk7NVfy7CxaK84Pojq734ocaJ3rdVpYfD9JsOdtUR4tWvYZufwGfcVBOAMOckXbvs9KvexfycF/RKbQ==, tarball: file:projects/text-editor.tgz} + resolution: {integrity: sha512-/TK8U02uYlCsPKiYVNS7t+UMmDxEPWLwY05soaV+Yuu3nXRGt2m2CkEFnv8I5IAqsEzgogKhqFXAgcQHcjfG+A==, tarball: file:projects/text-editor.tgz} id: file:projects/text-editor.tgz name: '@rush-temp/text-editor' version: 0.0.0 dependencies: - '@hocuspocus/provider': 2.11.1(bufferutil@4.0.8)(yjs@13.6.12) + '@hocuspocus/provider': 2.11.2(bufferutil@4.0.8)(yjs@13.6.12) '@tiptap/core': 2.2.3(@tiptap/pm@2.2.3) '@tiptap/extension-bubble-menu': 2.2.3(@tiptap/core@2.2.3)(@tiptap/pm@2.2.3) '@tiptap/extension-code': 2.2.3(@tiptap/core@2.2.3) @@ -22953,19 +23088,19 @@ packages: eslint-plugin-promise: 6.1.1(eslint@8.56.0) eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.11)(ts-node@10.9.2) jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) - lib0: 0.2.88 + lib0: 0.2.89 png-chunks-extract: 1.0.0 prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) prosemirror-codemark: 0.4.2(prosemirror-model@1.19.4) rfc6902: 5.1.1 - sass: 1.70.0 + sass: 1.71.0 slugify: 1.6.6 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 y-prosemirror: 1.2.2(prosemirror-model@1.19.4)(yjs@13.6.12) @@ -23071,12 +23206,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -23099,7 +23234,7 @@ packages: dev: false file:projects/tool.tgz(bufferutil@4.0.8)(svelte@4.2.11): - resolution: {integrity: sha512-6Qj2jQYlhNs5B5H6HIKdqGGa3CWPzqBIE6JrBMiAhauv09CXVGLum+wgAdcyrROzbfvJDg5vuPA80wi3cuN86w==, tarball: file:projects/tool.tgz} + resolution: {integrity: sha512-7SvydeuLUJiHLfgw7qRAuhTOopzoN6XU4XlWoQoDi+YYE4fj+c6PCZPP3GVotIOXCC3uNZe4wrFyrGClRhU0IA==, tarball: file:projects/tool.tgz} id: file:projects/tool.tgz name: '@rush-temp/tool' version: 0.0.0 @@ -23209,12 +23344,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -23326,12 +23461,12 @@ packages: just-clone: 6.2.0 prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -23404,12 +23539,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -23514,12 +23649,12 @@ packages: jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) - sass: 1.70.0 + sass: 1.71.0 svelte: 4.2.11 - svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11) + svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11) svelte-eslint-parser: 0.33.1(svelte@4.2.11) svelte-loader: 3.1.9(svelte@4.2.11) - svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.11)(typescript@5.3.3) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.0)(svelte@4.2.11)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.0)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 94bebd0ba2..6543eb83c7 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -84,6 +84,7 @@ services: - TRANSACTOR_URL=ws://localhost:3333 - ELASTIC_URL=http://elastic:9200 - COLLABORATOR_URL=ws://localhost:3078 + - COLLABORATOR_API_URL=http://localhost:3078 - MINIO_ENDPOINT=minio - MINIO_ACCESS_KEY=minioadmin - MINIO_SECRET_KEY=minioadmin diff --git a/dev/prod/.env b/dev/prod/.env index bbc6cf316e..64621b7ded 100644 --- a/dev/prod/.env +++ b/dev/prod/.env @@ -8,3 +8,4 @@ FRONT_URL=http://localhost:8080 REKONI_URL=http://localhost:4004 COLLABORATOR_URL=ws://locahost:3078 +COLLABORATOR_API_URL=http://locahost:3078 diff --git a/dev/prod/config.json b/dev/prod/config.json index a6a3245932..643bcbbd45 100644 --- a/dev/prod/config.json +++ b/dev/prod/config.json @@ -2,5 +2,6 @@ "ACCOUNTS_URL":"http://localhost:3000", "UPLOAD_URL":"/files", "COLLABORATOR_URL": "ws://localhost:3078", + "COLLABORATOR_API_URL": "http://localhost:3078", "REKONI_URL": "http://localhost:4004" } \ No newline at end of file diff --git a/dev/prod/package.json b/dev/prod/package.json index 01f9ab02eb..715068b811 100644 --- a/dev/prod/package.json +++ b/dev/prod/package.json @@ -104,6 +104,8 @@ "@hcengineering/inventory-resources": "^0.6.0", "@hcengineering/server-attachment": "^0.6.1", "@hcengineering/server-attachment-resources": "^0.6.0", + "@hcengineering/server-collaboration": "^0.6.0", + "@hcengineering/server-collaboration-resources": "^0.6.0", "@hcengineering/server-contact": "^0.6.1", "@hcengineering/server-contact-resources": "^0.6.0", "@hcengineering/server-notification": "^0.6.1", diff --git a/dev/prod/public/config-dev.json b/dev/prod/public/config-dev.json index 842ed6d3b8..a1af7ca5fb 100644 --- a/dev/prod/public/config-dev.json +++ b/dev/prod/public/config-dev.json @@ -6,5 +6,6 @@ "GMAIL_URL": "https://gmail.hc.engineering", "CALENDAR_URL": "https://calendar.hc.engineering", "REKONI_URL": "https://rekoni.hc.engineering", - "COLLABORATOR_URL": "wss://collaborator.hc.engineering" + "COLLABORATOR_URL": "wss://collaborator.hc.engineering", + "COLLABORATOR_API_URL": "https://collaborator.hc.engineering" } \ No newline at end of file diff --git a/dev/prod/public/config.json b/dev/prod/public/config.json index 5670346aea..22b22e1e3f 100644 --- a/dev/prod/public/config.json +++ b/dev/prod/public/config.json @@ -7,5 +7,6 @@ "CALENDAR_URL": "http://localhost:8095", "REKONI_URL": "http://localhost:4004", "COLLABORATOR_URL": "ws://localhost:3078", + "COLLABORATOR_API_URL": "http://localhost:3078", "LAST_NAME_FIRST": "true" } \ No newline at end of file diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index 1036fb2394..c0da00fe79 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -89,6 +89,7 @@ interface Config { GMAIL_URL: string CALENDAR_URL: string COLLABORATOR_URL: string + COLLABORATOR_API_URL: string TITLE?: string LANGUAGES?: string DEFAULT_LANGUAGE?: string @@ -139,6 +140,7 @@ export async function configurePlatform() { setMetadata(login.metadata.AccountsUrl, config.ACCOUNTS_URL) setMetadata(presentation.metadata.UploadURL, config.UPLOAD_URL) setMetadata(presentation.metadata.CollaboratorUrl, config.COLLABORATOR_URL) + setMetadata(presentation.metadata.CollaboratorApiUrl, config.COLLABORATOR_API_URL) if (config.MODEL_VERSION != null) { console.log('Minimal Model version requirement', config.MODEL_VERSION) diff --git a/dev/tool/package.json b/dev/tool/package.json index bd38c80ca0..7e78f4afa7 100644 --- a/dev/tool/package.json +++ b/dev/tool/package.json @@ -76,6 +76,8 @@ "@hcengineering/rekoni": "^0.6.0", "@hcengineering/server-attachment": "^0.6.1", "@hcengineering/server-attachment-resources": "^0.6.0", + "@hcengineering/server-collaboration": "^0.6.0", + "@hcengineering/server-collaboration-resources": "^0.6.0", "@hcengineering/server-backup": "^0.6.0", "@hcengineering/server-calendar": "^0.6.0", "@hcengineering/server-calendar-resources": "^0.6.0", diff --git a/dev/tool/src/__start.ts b/dev/tool/src/__start.ts index f7b641bf5c..10785ddb16 100644 --- a/dev/tool/src/__start.ts +++ b/dev/tool/src/__start.ts @@ -24,6 +24,7 @@ import { devTool } from '.' import { addLocation } from '@hcengineering/platform' import { serverActivityId } from '@hcengineering/server-activity' import { serverAttachmentId } from '@hcengineering/server-attachment' +import { serverCollaborationId } from '@hcengineering/server-collaboration' import { serverCalendarId } from '@hcengineering/server-calendar' import { serverChunterId } from '@hcengineering/server-chunter' import { serverContactId } from '@hcengineering/server-contact' @@ -43,6 +44,7 @@ import { serverViewId } from '@hcengineering/server-view' addLocation(serverActivityId, () => import('@hcengineering/server-activity-resources')) addLocation(serverAttachmentId, () => import('@hcengineering/server-attachment-resources')) +addLocation(serverCollaborationId, () => import('@hcengineering/server-collaboration-resources')) addLocation(serverContactId, () => import('@hcengineering/server-contact-resources')) addLocation(serverNotificationId, () => import('@hcengineering/server-notification-resources')) addLocation(serverChunterId, () => import('@hcengineering/server-chunter-resources')) diff --git a/models/all/package.json b/models/all/package.json index 90abb824f3..1951d761ab 100644 --- a/models/all/package.json +++ b/models/all/package.json @@ -41,6 +41,7 @@ "@hcengineering/model-telegram": "^0.6.0", "@hcengineering/model-server-core": "^0.6.0", "@hcengineering/model-server-attachment": "^0.6.0", + "@hcengineering/model-server-collaboration": "^0.6.0", "@hcengineering/model-server-contact": "^0.6.0", "@hcengineering/model-server-notification": "^0.6.0", "@hcengineering/model-server-setting": "^0.6.0", diff --git a/models/all/src/index.ts b/models/all/src/index.ts index d947fc4760..c8cc054956 100644 --- a/models/all/src/index.ts +++ b/models/all/src/index.ts @@ -35,6 +35,10 @@ import recruit, { recruitId, createModel as recruitModel } from '@hcengineering/ import { requestId, createModel as requestModel } from '@hcengineering/model-request' import { serverActivityId, createModel as serverActivityModel } from '@hcengineering/model-server-activity' import { serverAttachmentId, createModel as serverAttachmentModel } from '@hcengineering/model-server-attachment' +import { + serverCollaborationId, + createModel as serverCollaborationModel +} from '@hcengineering/model-server-collaboration' import { serverCalendarId, createModel as serverCalendarModel } from '@hcengineering/model-server-calendar' import { serverChunterId, createModel as serverChunterModel } from '@hcengineering/model-server-chunter' import { serverContactId, createModel as serverContactModel } from '@hcengineering/model-server-contact' @@ -274,6 +278,7 @@ export default function buildModel (enabled: string[] = ['*'], disabled: string[ [serverCoreModel, serverCoreId], [serverAttachmentModel, serverAttachmentId], + [serverCollaborationModel, serverCollaborationId], [serverContactModel, serverContactId], [serveSettingModel, serverSettingId], [serverChunterModel, serverChunterId], diff --git a/models/core/src/core.ts b/models/core/src/core.ts index 3e68b08dbb..f94244d8fe 100644 --- a/models/core/src/core.ts +++ b/models/core/src/core.ts @@ -355,3 +355,11 @@ export class TIndexConfiguration extends TClass implements indexes!: FieldIndex[] searchDisabled!: boolean } + +@UX(core.string.CollaborativeDoc) +@Model(core.class.TypeCollaborativeDoc, core.class.Type) +export class TTypeCollaborativeDoc extends TType {} + +@UX(core.string.CollaborativeDocVersion) +@Model(core.class.TypeCollaborativeDocVersion, core.class.Type) +export class TTypeCollaborativeDocVersion extends TType {} diff --git a/models/core/src/index.ts b/models/core/src/index.ts index fb2867f340..bed9a3585c 100644 --- a/models/core/src/index.ts +++ b/models/core/src/index.ts @@ -52,6 +52,8 @@ import { TTypeAny, TTypeAttachment, TTypeBoolean, + TTypeCollaborativeDoc, + TTypeCollaborativeDocVersion, TTypeCollaborativeMarkup, TTypeDate, TTypeHyperlink, @@ -110,6 +112,8 @@ export function createModel (builder: Builder): void { TType, TEnumOf, TTypeMarkup, + TTypeCollaborativeDoc, + TTypeCollaborativeDocVersion, TTypeCollaborativeMarkup, TArrOf, TRefTo, diff --git a/models/server-collaboration/.eslintrc.js b/models/server-collaboration/.eslintrc.js new file mode 100644 index 0000000000..c1cf82cba0 --- /dev/null +++ b/models/server-collaboration/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/models/server-collaboration/.npmignore b/models/server-collaboration/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/models/server-collaboration/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/models/server-collaboration/config/rig.json b/models/server-collaboration/config/rig.json new file mode 100644 index 0000000000..2f6be36605 --- /dev/null +++ b/models/server-collaboration/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "model" +} diff --git a/models/server-collaboration/package.json b/models/server-collaboration/package.json new file mode 100644 index 0000000000..ebd3f10678 --- /dev/null +++ b/models/server-collaboration/package.json @@ -0,0 +1,36 @@ +{ + "name": "@hcengineering/model-server-collaboration", + "version": "0.6.0", + "main": "lib/index.js", + "author": "Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent", + "_phase:build": "compile", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src" + }, + "template": "@hcengineering/model-package", + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.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.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "prettier-plugin-svelte": "^3.1.0" + }, + "dependencies": { + "@hcengineering/core": "^0.6.28", + "@hcengineering/model": "^0.6.7", + "@hcengineering/platform": "^0.6.9", + "@hcengineering/server-collaboration": "^0.6.0", + "@hcengineering/server-core": "^0.6.1" + } +} diff --git a/models/server-collaboration/src/index.ts b/models/server-collaboration/src/index.ts new file mode 100644 index 0000000000..30e6796737 --- /dev/null +++ b/models/server-collaboration/src/index.ts @@ -0,0 +1,28 @@ +// +// 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 Builder } from '@hcengineering/model' + +import core from '@hcengineering/core' +import serverCollaboration from '@hcengineering/server-collaboration' +import serverCore from '@hcengineering/server-core' + +export { serverCollaborationId } from '@hcengineering/server-collaboration' + +export function createModel (builder: Builder): void { + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverCollaboration.trigger.OnDelete + }) +} diff --git a/models/server-collaboration/tsconfig.json b/models/server-collaboration/tsconfig.json new file mode 100644 index 0000000000..cce8988c63 --- /dev/null +++ b/models/server-collaboration/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 179375c729..e4e09d0d01 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -512,6 +512,14 @@ export function createModel (builder: Builder): void { editor: view.component.CollaborativeHTMLEditor }) + builder.mixin(core.class.TypeCollaborativeDoc, core.class.Class, view.mixin.InlineAttributEditor, { + editor: view.component.CollaborativeDocEditor + }) + + builder.mixin(core.class.TypeCollaborativeDocVersion, core.class.Class, view.mixin.InlineAttributEditor, { + editor: view.component.CollaborativeDocEditor + }) + classPresenter(builder, core.class.TypeBoolean, view.component.BooleanPresenter, view.component.BooleanEditor) classPresenter( builder, diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index e3f646000b..97720550e7 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -70,6 +70,7 @@ export default mergeIds(viewId, view, { EnumArrayEditor: '' as AnyComponent, HTMLEditor: '' as AnyComponent, CollaborativeHTMLEditor: '' as AnyComponent, + CollaborativeDocEditor: '' as AnyComponent, MarkupEditor: '' as AnyComponent, MarkupEditorPopup: '' as AnyComponent, ListView: '' as AnyComponent, diff --git a/packages/collaborator-client/src/client.ts b/packages/collaborator-client/src/client.ts index fcebfb087d..28aa40b428 100644 --- a/packages/collaborator-client/src/client.ts +++ b/packages/collaborator-client/src/client.ts @@ -13,20 +13,111 @@ // limitations under the License. // -import { Class, Doc, Hierarchy, Markup, Ref, WorkspaceId, concatLink } from '@hcengineering/core' -import { minioDocumentId, mongodbDocumentId } from './utils' +import { + Account, + Class, + CollaborativeDoc, + Doc, + Hierarchy, + Markup, + Ref, + Timestamp, + WorkspaceId, + concatLink, + toCollaborativeDocVersion +} from '@hcengineering/core' +import { DocumentURI, collaborativeDocumentUri, mongodbDocumentUri } from './uri' -/** - * @public - */ -export interface CollaboratorClient { - get: (classId: Ref>, docId: Ref, attribute: string) => Promise - update: (classId: Ref>, docId: Ref, attribute: string, value: Markup) => Promise +/** @public */ +export interface GetContentRequest { + documentId: DocumentURI + field: string } -/** - * @public - */ +/** @public */ +export interface GetContentResponse { + html: string +} + +/** @public */ +export interface UpdateContentRequest { + documentId: DocumentURI + field: string + html: string +} + +/** @public */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UpdateContentResponse {} + +/** @public */ +export interface CopyContentRequest { + documentId: DocumentURI + sourceField: string + targetField: string +} + +/** @public */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CopyContentResponse {} + +/** @public */ +export interface BranchDocumentRequest { + sourceDocumentId: DocumentURI + targetDocumentId: DocumentURI +} + +/** @public */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface BranchDocumentResponse {} + +/** @public */ +export interface RemoveDocumentRequest { + documentId: DocumentURI + collaborativeDoc: CollaborativeDoc +} + +/** @public */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RemoveDocumentResponse {} + +/** @public */ +export interface TakeSnapshotRequest { + documentId: DocumentURI + collaborativeDoc: CollaborativeDoc + createdBy: string + snapshotName: string +} + +/** @public */ +export interface TakeSnapshotResponse { + versionId: string + name: string + + createdBy: string + createdOn: Timestamp +} + +/** @public */ +export interface CollaborativeDocSnapshotParams { + snapshotName: string + createdBy: Ref +} + +/** @public */ +export interface CollaboratorClient { + // field operations + getContent: (collaborativeDoc: CollaborativeDoc, field: string) => Promise + updateContent: (collaborativeDoc: CollaborativeDoc, field: string, value: Markup) => Promise + copyContent: (collaborativeDoc: CollaborativeDoc, sourceField: string, targetField: string) => Promise + + // document operations + branch: (source: CollaborativeDoc, target: CollaborativeDoc) => Promise + remove: (collaborativeDoc: CollaborativeDoc) => Promise + snapshot: (collaborativeDoc: CollaborativeDoc, params: CollaborativeDocSnapshotParams) => Promise +} + +/** @public */ export function getClient ( hierarchy: Hierarchy, workspaceId: WorkspaceId, @@ -44,51 +135,85 @@ class CollaboratorClientImpl implements CollaboratorClient { private readonly collaboratorUrl: string ) {} - initialContentId (workspace: string, classId: Ref>, docId: Ref, attribute: string): string { + initialContentId (workspace: string, classId: Ref>, docId: Ref, attribute: string): DocumentURI { const domain = this.hierarchy.getDomain(classId) - return mongodbDocumentId(workspace, domain, docId, attribute) + return mongodbDocumentUri(workspace, domain, docId, attribute) } - async get (classId: Ref>, docId: Ref, attribute: string): Promise { - const workspace = this.workspace.name - const documentId = encodeURIComponent(minioDocumentId(workspace, docId, attribute)) - const initialContentId = encodeURIComponent(this.initialContentId(workspace, classId, docId, attribute)) - attribute = encodeURIComponent(attribute) - - const url = concatLink( - this.collaboratorUrl, - `/api/content/${documentId}/${attribute}?initialContentId=${initialContentId}` - ) + private async rpc (method: string, payload: any): Promise { + const url = concatLink(this.collaboratorUrl, '/rpc') const res = await fetch(url, { - method: 'GET', - headers: { - Authorization: 'Bearer ' + this.token, - Accept: 'application/json' - } - }) - const json = await res.json() - return json.html ?? '

' - } - - async update (classId: Ref>, docId: Ref, attribute: string, value: Markup): Promise { - const workspace = this.workspace.name - const documentId = encodeURIComponent(minioDocumentId(workspace, docId, attribute)) - const initialContentId = encodeURIComponent(this.initialContentId(workspace, classId, docId, attribute)) - attribute = encodeURIComponent(attribute) - - const url = concatLink( - this.collaboratorUrl, - `/api/content/${documentId}/${attribute}?initialContentId=${initialContentId}` - ) - - await fetch(url, { - method: 'PUT', + method: 'POST', headers: { Authorization: 'Bearer ' + this.token, 'Content-Type': 'application/json' }, - body: JSON.stringify({ html: value }) + body: JSON.stringify({ method, payload }) }) + + const result = await res.json() + + if (result.error != null) { + throw new Error(result.error) + } + + return result + } + + async getContent (collaborativeDoc: CollaborativeDoc, field: string): Promise { + const workspace = this.workspace.name + const documentId = collaborativeDocumentUri(workspace, collaborativeDoc) + + const payload: GetContentRequest = { documentId, field } + const res = (await this.rpc('getContent', payload)) as GetContentResponse + + return res.html ?? '' + } + + async updateContent (collaborativeDoc: CollaborativeDoc, field: string, value: Markup): Promise { + const workspace = this.workspace.name + const documentId = collaborativeDocumentUri(workspace, collaborativeDoc) + + const payload: UpdateContentRequest = { documentId, field, html: value } + await this.rpc('updateContent', payload) + } + + async copyContent (collaborativeDoc: CollaborativeDoc, sourceField: string, targetField: string): Promise { + const workspace = this.workspace.name + const documentId = collaborativeDocumentUri(workspace, collaborativeDoc) + + const payload: CopyContentRequest = { documentId, sourceField, targetField } + await this.rpc('copyContent', payload) + } + + async branch (source: CollaborativeDoc, target: CollaborativeDoc): Promise { + const workspace = this.workspace.name + const sourceDocumentId = collaborativeDocumentUri(workspace, source) + const targetDocumentId = collaborativeDocumentUri(workspace, target) + + const payload: BranchDocumentRequest = { sourceDocumentId, targetDocumentId } + await this.rpc('branchDocument', payload) + } + + async remove (collaborativeDoc: CollaborativeDoc): Promise { + const workspace = this.workspace.name + const documentId = collaborativeDocumentUri(workspace, collaborativeDoc) + + const payload: RemoveDocumentRequest = { documentId, collaborativeDoc } + await this.rpc('removeDocument', payload) + } + + async snapshot ( + collaborativeDoc: CollaborativeDoc, + params: CollaborativeDocSnapshotParams + ): Promise { + const workspace = this.workspace.name + const documentId = collaborativeDocumentUri(workspace, collaborativeDoc) + + const payload: TakeSnapshotRequest = { documentId, collaborativeDoc, ...params } + const res = (await this.rpc('takeSnapshot', payload)) as TakeSnapshotResponse + + return toCollaborativeDocVersion(collaborativeDoc, res.versionId) } } diff --git a/packages/collaborator-client/src/index.ts b/packages/collaborator-client/src/index.ts index 2ac44b392f..5f0d8d2aba 100644 --- a/packages/collaborator-client/src/index.ts +++ b/packages/collaborator-client/src/index.ts @@ -13,5 +13,6 @@ // limitations under the License. // -export { type CollaboratorClient, getClient } from './client' +export * from './client' export * from './utils' +export * from './uri' diff --git a/packages/collaborator-client/src/uri.ts b/packages/collaborator-client/src/uri.ts new file mode 100644 index 0000000000..f8dec9003f --- /dev/null +++ b/packages/collaborator-client/src/uri.ts @@ -0,0 +1,41 @@ +// +// 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, Domain, Ref, parseCollaborativeDoc } from '@hcengineering/core' + +export type DocumentURI = string & { __documentUri: true } + +export function collaborativeDocumentUri (workspaceUrl: string, docId: CollaborativeDoc): DocumentURI { + const { documentId, versionId } = parseCollaborativeDoc(docId) + return `minio://${workspaceUrl}/${documentId}/${versionId}` as DocumentURI +} + +export function platformDocumentUri ( + workspaceUrl: string, + objectClass: Ref>, + objectId: Ref, + objectAttr: string +): DocumentURI { + return `platform://${workspaceUrl}/${objectClass}/${objectId}/${objectAttr}` as DocumentURI +} + +export function mongodbDocumentUri ( + workspaceUrl: string, + domain: Domain, + docId: Ref, + objectAttr: string +): DocumentURI { + return `mongodb://${workspaceUrl}/${domain}/${docId}/${objectAttr}` as DocumentURI +} diff --git a/packages/core/src/__tests__/collaboration.test.ts b/packages/core/src/__tests__/collaboration.test.ts new file mode 100644 index 0000000000..6d46ae0a5c --- /dev/null +++ b/packages/core/src/__tests__/collaboration.test.ts @@ -0,0 +1,63 @@ +// +// 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 { + CollaborativeDoc, + formatCollaborativeDoc, + formatCollaborativeDocVersion, + parseCollaborativeDoc +} from '../collaboration' + +describe('collaborative-doc', () => { + describe('parseCollaborativeDoc', () => { + it('parses collaborative doc id', async () => { + expect(parseCollaborativeDoc('minioDocumentId:HEAD:0' as CollaborativeDoc)).toEqual({ + documentId: 'minioDocumentId', + versionId: 'HEAD', + revisionId: '0' + }) + }) + it('parses collaborative doc version id', async () => { + expect(parseCollaborativeDoc('minioDocumentId:main' as CollaborativeDoc)).toEqual({ + documentId: 'minioDocumentId', + versionId: 'main', + revisionId: 'main' + }) + }) + }) + + describe('formatCollaborativeDoc', () => { + it('returns valid collaborative doc id', async () => { + expect( + formatCollaborativeDoc({ + documentId: 'minioDocumentId', + versionId: 'HEAD', + revisionId: '0' + }) + ).toEqual('minioDocumentId:HEAD:0') + }) + }) + + describe('formatCollaborativeDocVersion', () => { + it('returns valid collaborative doc id', async () => { + expect( + formatCollaborativeDocVersion({ + documentId: 'minioDocumentId', + versionId: 'versionId' + }) + ).toEqual('minioDocumentId:versionId') + }) + }) +}) diff --git a/packages/core/src/collaboration.ts b/packages/core/src/collaboration.ts new file mode 100644 index 0000000000..214b5e5a52 --- /dev/null +++ b/packages/core/src/collaboration.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 { Doc, Ref } from './classes' + +/** + * Identifier of the collaborative document holding collaborative content. + * + * Format: + * {minioDocumentId}:{versionId}:{revisionId} + * {minioDocumentId}:{versionId} + * + * @public + * */ +export type CollaborativeDoc = string & { __collaborativeDocId: true } + +/** @public */ +export type CollaborativeDocVersion = string | typeof CollaborativeDocVersionHead + +/** @public */ +export const CollaborativeDocVersionHead = 'HEAD' + +/** @public */ +export function getCollaborativeDocId (objectId: Ref, objectAttr?: string | undefined): string { + return objectAttr !== undefined && objectAttr !== '' ? `${objectId}%${objectAttr}` : `${objectId}` +} + +/** @public */ +export function getCollaborativeDoc (documentId: string): CollaborativeDoc { + return formatCollaborativeDoc({ + documentId, + versionId: CollaborativeDocVersionHead, + revisionId: '0' + }) +} + +/** @public */ +export interface CollaborativeDocData { + documentId: string + versionId: CollaborativeDocVersion + revisionId: string +} + +/** @public */ +export function parseCollaborativeDoc (id: CollaborativeDoc): CollaborativeDocData { + const [documentId, versionId, revisionId] = id.split(':') + return { documentId, versionId, revisionId: revisionId ?? versionId } +} + +/** @public */ +export function formatCollaborativeDoc ({ documentId, versionId, revisionId }: CollaborativeDocData): CollaborativeDoc { + return `${documentId}:${versionId}:${revisionId}` as CollaborativeDoc +} + +/** @public */ +export function formatCollaborativeDocVersion ({ + documentId, + versionId +}: Omit): CollaborativeDoc { + return `${documentId}:${versionId}` as CollaborativeDoc +} + +/** @public */ +export function toCollaborativeDocVersion (collaborativeDoc: CollaborativeDoc, versionId: string): CollaborativeDoc { + const { documentId } = parseCollaborativeDoc(collaborativeDoc) + return formatCollaborativeDocVersion({ documentId, versionId }) +} diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 9addc59fbd..a72ab3e47b 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -48,6 +48,7 @@ import type { TypeAny, UserStatus } from './classes' +import { CollaborativeDoc } from './collaboration' import { Status, StatusCategory } from './status' import type { Tx, @@ -104,6 +105,8 @@ export default plugin(coreId, { TypeBoolean: '' as Ref>>, TypeTimestamp: '' as Ref>>, TypeDate: '' as Ref>>, + TypeCollaborativeDoc: '' as Ref>>, + TypeCollaborativeDocVersion: '' as Ref>>, TypeCollaborativeMarkup: '' as Ref>>, RefTo: '' as Ref>>, ArrOf: '' as Ref>>, @@ -163,6 +166,8 @@ export default plugin(coreId, { Record: '' as IntlString, Markup: '' as IntlString, Collaborative: '' as IntlString, + CollaborativeDoc: '' as IntlString, + CollaborativeDocVersion: '' as IntlString, Number: '' as IntlString, Boolean: '' as IntlString, Timestamp: '' as IntlString, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a38d06dbe5..4303f8e73d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -15,6 +15,7 @@ // export * from './classes' export * from './client' +export * from './collaboration' export { coreId, systemAccountEmail, default } from './component' export * from './hierarchy' export * from './measurements' diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts index 7596321f7a..f2f1ad17b2 100644 --- a/packages/core/src/tx.ts +++ b/packages/core/src/tx.ts @@ -452,6 +452,22 @@ export abstract class TxProcessor implements WithTx { return tx } + 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 diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index ed81d3d416..5f55cad8b5 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -184,7 +184,8 @@ export function isFullTextAttribute (attr: AnyAttribute): boolean { return ( attr.index === IndexKind.FullText || attr.type._class === core.class.TypeAttachment || - attr.type._class === core.class.EnumOf + attr.type._class === core.class.EnumOf || + attr.type._class === core.class.TypeCollaborativeDoc ) } diff --git a/packages/model/src/dsl.ts b/packages/model/src/dsl.ts index 944ff9a18d..7e87d83344 100644 --- a/packages/model/src/dsl.ts +++ b/packages/model/src/dsl.ts @@ -20,6 +20,7 @@ import core, { Class, Classifier, ClassifierKind, + CollaborativeDoc, Data, DateRangeMode, Doc, @@ -489,3 +490,17 @@ export function Collection (clazz: Ref>, itemLab export function ArrOf> (type: Type): TypeArrOf { return { _class: core.class.ArrOf, label: core.string.Array, of: type } } + +/** + * @public + */ +export function TypeCollaborativeDoc (): Type { + return { _class: core.class.TypeCollaborativeDoc, label: core.string.CollaborativeDoc } +} + +/** + * @public + */ +export function TypeCollaborativeDocVersion (): Type { + return { _class: core.class.TypeCollaborativeDocVersion, label: core.string.CollaborativeDocVersion } +} diff --git a/packages/presentation/src/collaborator.ts b/packages/presentation/src/collaborator.ts index e9a1f8baa0..8da19a5176 100644 --- a/packages/presentation/src/collaborator.ts +++ b/packages/presentation/src/collaborator.ts @@ -14,43 +14,58 @@ // import { type CollaboratorClient, getClient as getCollaborator } from '@hcengineering/collaborator-client' -import { getWorkspaceId, type Class, type Doc, type Markup, type Ref } from '@hcengineering/core' +import { type CollaborativeDoc, type Markup, getCurrentAccount, getWorkspaceId } from '@hcengineering/core' import { getMetadata } from '@hcengineering/platform' import { getCurrentLocation } from '@hcengineering/ui' import { getClient } from '.' import presentation from './plugin' -/** - * @public - */ +/** @public */ export function getCollaboratorClient (): CollaboratorClient { const workspaceId = getWorkspaceId(getCurrentLocation().path[1] ?? '') const hierarchy = getClient().getHierarchy() const token = getMetadata(presentation.metadata.Token) ?? '' - const collaboratorURL = getMetadata(presentation.metadata.CollaboratorUrl) ?? '' + const collaboratorURL = getMetadata(presentation.metadata.CollaboratorApiUrl) ?? '' return getCollaborator(hierarchy, workspaceId, token, collaboratorURL) } -/** - * @public - */ -export async function getMarkup (classId: Ref>, docId: Ref, attribute: string): Promise { +/** @public */ +export async function getMarkup (collaborativeDoc: CollaborativeDoc, field: string): Promise { const client = getCollaboratorClient() - return await client.get(classId, docId, attribute) + return await client.getContent(collaborativeDoc, field) } -/** - * @public - */ -export async function updateMarkup ( - classId: Ref>, - docId: Ref, - attribute: string, - value: Markup +/** @public */ +export async function updateMarkup (collaborativeDoc: CollaborativeDoc, field: string, value: Markup): Promise { + const client = getCollaboratorClient() + await client.updateContent(collaborativeDoc, field, value) +} + +/** @public */ +export async function copyDocumentContent ( + collaborativeDoc: CollaborativeDoc, + sourceField: string, + targetField: string ): Promise { const client = getCollaboratorClient() - - await client.update(classId, docId, attribute, value) + await client.copyContent(collaborativeDoc, sourceField, targetField) +} + +/** @public */ +export async function copyDocument (source: CollaborativeDoc, target: CollaborativeDoc): Promise { + const client = getCollaboratorClient() + await client.branch(source, target) +} + +/** @public */ +export async function takeSnapshot ( + collaborativeDoc: CollaborativeDoc, + snapshotName: string +): Promise { + const client = getCollaboratorClient() + const createdBy = getCurrentAccount()._id + + return await client.snapshot(collaborativeDoc, { createdBy, snapshotName }) } diff --git a/packages/presentation/src/plugin.ts b/packages/presentation/src/plugin.ts index 69ba828440..70f8615840 100644 --- a/packages/presentation/src/plugin.ts +++ b/packages/presentation/src/plugin.ts @@ -78,6 +78,7 @@ export default plugin(presentationId, { Draft: '' as Metadata>, UploadURL: '' as Metadata, CollaboratorUrl: '' as Metadata, + CollaboratorApiUrl: '' as Metadata, Token: '' as Metadata, FrontUrl: '' as Asset } diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index 291661b8df..7a5b5eddd8 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -409,6 +409,9 @@ export function getAttributePresenterClass ( if (hierarchy.isDerived(attrClass, core.class.TypeCollaborativeMarkup)) { category = 'inplace' } + if (hierarchy.isDerived(attrClass, core.class.TypeCollaborativeDoc)) { + category = 'inplace' + } if (hierarchy.isDerived(attrClass, core.class.Collection)) { attrClass = (attribute.type as Collection).of category = 'collection' diff --git a/packages/text-editor/package.json b/packages/text-editor/package.json index 26cacbbe06..c39ba12280 100644 --- a/packages/text-editor/package.json +++ b/packages/text-editor/package.json @@ -44,6 +44,7 @@ "@hcengineering/ui": "^0.6.11", "@hcengineering/view": "^0.6.9", "@hcengineering/text": "^0.6.1", + "@hcengineering/collaborator-client": "^0.6.0", "svelte": "^4.2.5", "@tiptap/core": "^2.1.12", "@tiptap/pm": "^2.1.12", diff --git a/packages/text-editor/src/components/CollaborativeAttributeBox.svelte b/packages/text-editor/src/components/CollaborativeAttributeBox.svelte index e55b00a7f5..12ecf68e7a 100644 --- a/packages/text-editor/src/components/CollaborativeAttributeBox.svelte +++ b/packages/text-editor/src/components/CollaborativeAttributeBox.svelte @@ -13,15 +13,16 @@ // limitations under the License. --> {#key object?._id} diff --git a/plugins/view-resources/src/components/CollaborativeDocEditor.svelte b/plugins/view-resources/src/components/CollaborativeDocEditor.svelte new file mode 100644 index 0000000000..15daeccd24 --- /dev/null +++ b/plugins/view-resources/src/components/CollaborativeDocEditor.svelte @@ -0,0 +1,29 @@ + + + +{#key object._id} + {#key key.key} + + {/key} +{/key} diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 57a1b83a92..8c6f3ae337 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -43,6 +43,7 @@ import StringFilter from './components/filter/StringFilter.svelte' import StringFilterPresenter from './components/filter/StringFilterPresenter.svelte' import TimestampFilter from './components/filter/TimestampFilter.svelte' import ValueFilter from './components/filter/ValueFilter.svelte' +import CollaborativeDocEditor from './components/CollaborativeDocEditor.svelte' import CollaborativeHTMLEditor from './components/CollaborativeHTMLEditor.svelte' import HTMLEditor from './components/HTMLEditor.svelte' import HTMLPresenter from './components/HTMLPresenter.svelte' @@ -246,6 +247,7 @@ export default async (): Promise => ({ FilterTypePopup, ValueSelector, HTMLEditor, + CollaborativeDocEditor, CollaborativeHTMLEditor, ListView, GrowPresenter, diff --git a/pods/front/run.sh b/pods/front/run.sh index 3c595f7bd6..faf33964f6 100755 --- a/pods/front/run.sh +++ b/pods/front/run.sh @@ -5,6 +5,7 @@ export UPLOAD_URL=http://localhost:3333/files export TRANSACTOR_URL=ws://localhost:3333 export ELASTIC_URL=http://elastic:9200 export COLLABORATOR_URL=ws://localhost:3078 +export COLLABORATOR_API_URL=http://localhost:3078 export MINIO_ENDPOINT=minio export MINIO_ACCESS_KEY=minioadmin export MINIO_SECRET_KEY=minioadmin diff --git a/pods/server/package.json b/pods/server/package.json index fd74b1c6f0..f66bfcdb2c 100644 --- a/pods/server/package.json +++ b/pods/server/package.json @@ -52,6 +52,8 @@ "@hcengineering/server-ws": "^0.6.11", "@hcengineering/server-attachment": "^0.6.1", "@hcengineering/server-attachment-resources": "^0.6.0", + "@hcengineering/server-collaboration": "^0.6.0", + "@hcengineering/server-collaboration-resources": "^0.6.0", "@hcengineering/server": "^0.6.4", "@hcengineering/mongo": "^0.6.1", "@hcengineering/elastic": "^0.6.0", diff --git a/pods/server/src/server.ts b/pods/server/src/server.ts index 756c132e38..9b7df710b4 100644 --- a/pods/server/src/server.ts +++ b/pods/server/src/server.ts @@ -47,6 +47,7 @@ import { type MinioConfig } from '@hcengineering/server' import { serverAttachmentId } from '@hcengineering/server-attachment' +import { CollaborativeContentRetrievalStage, serverCollaborationId } from '@hcengineering/server-collaboration' import { serverCalendarId } from '@hcengineering/server-calendar' import { serverChunterId } from '@hcengineering/server-chunter' import { serverContactId } from '@hcengineering/server-contact' @@ -193,6 +194,7 @@ export function start ( } ): () => Promise { addLocation(serverAttachmentId, () => import('@hcengineering/server-attachment-resources')) + addLocation(serverCollaborationId, () => import('@hcengineering/server-collaboration-resources')) addLocation(serverContactId, () => import('@hcengineering/server-contact-resources')) addLocation(serverNotificationId, () => import('@hcengineering/server-notification-resources')) addLocation(serverSettingId, () => import('@hcengineering/server-setting-resources')) @@ -241,6 +243,16 @@ export function start ( // Obtain text content from storage(like minio) and use content adapter to convert files to text content. stages.push(new ContentRetrievalStage(storageAdapter, workspace, fullText.newChild('content', {}), contentAdapter)) + // Obtain collaborative content + stages.push( + new CollaborativeContentRetrievalStage( + storageAdapter, + workspace, + fullText.newChild('collaborative', {}), + contentAdapter + ) + ) + // // Add any => english language translation // const retranslateStage = new LibRetranslateStage(fullText.newChild('retranslate', {}), workspace) // retranslateStage.clearExcept = stages.map(it => it.stageId) diff --git a/products/tracker/package.json b/products/tracker/package.json index 7ce84321a0..cbbcc5f4f7 100644 --- a/products/tracker/package.json +++ b/products/tracker/package.json @@ -88,6 +88,8 @@ "@hcengineering/image-cropper-resources": "^0.6.0", "@hcengineering/server-attachment": "^0.6.1", "@hcengineering/server-attachment-resources": "^0.6.0", + "@hcengineering/server-collaboration": "^0.6.0", + "@hcengineering/server-collaboration-resources": "^0.6.0", "@hcengineering/server-contact": "^0.6.1", "@hcengineering/server-contact-resources": "^0.6.0", "@hcengineering/server-notification": "^0.6.0", diff --git a/rush.json b/rush.json index 1346d5bbb9..c32ba1a4c6 100644 --- a/rush.json +++ b/rush.json @@ -466,6 +466,11 @@ "projectFolder": "packages/analytics", "shouldPublish": false }, + { + "packageName": "@hcengineering/collaboration", + "projectFolder": "server/collaboration", + "shouldPublish": false + }, { "packageName": "@hcengineering/server-ws", "projectFolder": "server/ws", @@ -746,6 +751,21 @@ "projectFolder": "server-plugins/attachment-resources", "shouldPublish": false }, + { + "packageName": "@hcengineering/server-collaboration", + "projectFolder": "server-plugins/collaboration", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/model-server-collaboration", + "projectFolder": "models/server-collaboration", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/server-collaboration-resources", + "projectFolder": "server-plugins/collaboration-resources", + "shouldPublish": false + }, { "packageName": "@hcengineering/server-contact", "projectFolder": "server-plugins/contact", diff --git a/server-plugins/chunter-resources/package.json b/server-plugins/chunter-resources/package.json index 450d97798b..86e3fed722 100644 --- a/server-plugins/chunter-resources/package.json +++ b/server-plugins/chunter-resources/package.json @@ -39,6 +39,7 @@ "@hcengineering/view": "^0.6.9", "@hcengineering/login": "^0.6.8", "@hcengineering/workbench": "^0.6.9", + "@hcengineering/collaboration": "^0.6.0", "@hcengineering/notification": "^0.6.16", "@hcengineering/server-notification": "^0.6.1", "@hcengineering/server-notification-resources": "^0.6.0", diff --git a/server-plugins/chunter-resources/src/backlinks.ts b/server-plugins/chunter-resources/src/backlinks.ts index 9fff89a4ff..36cc9df43c 100644 --- a/server-plugins/chunter-resources/src/backlinks.ts +++ b/server-plugins/chunter-resources/src/backlinks.ts @@ -14,10 +14,11 @@ // import chunter, { Backlink } from '@hcengineering/chunter' - +import { loadCollaborativeDoc, yDocToBuffer } from '@hcengineering/collaboration' import core, { AttachedDoc, Class, + CollaborativeDoc, Data, Doc, Hierarchy, @@ -26,25 +27,127 @@ import core, { TxCollectionCUD, TxCUD, TxFactory, - TxProcessor + TxProcessor, + Type } from '@hcengineering/core' -import { ServerKit, extractReferences, getHTML, parseHTML } from '@hcengineering/text' -import { TriggerControl } from '@hcengineering/server-core' import notification from '@hcengineering/notification' +import { ServerKit, extractReferences, getHTML, parseHTML, yDocContentToNodes } from '@hcengineering/text' +import { StorageAdapter, TriggerControl } from '@hcengineering/server-core' const extensions = [ServerKit] +export function isMarkupType (type: Ref>>): boolean { + return type === core.class.TypeMarkup || type === core.class.TypeCollaborativeMarkup +} + +export function isCollaborativeType (type: Ref>>): boolean { + return type === core.class.TypeCollaborativeDoc +} + +export async function getCreateBacklinksTxes ( + control: TriggerControl, + storage: StorageAdapter, + txFactory: TxFactory, + doc: Doc, + backlinkId: Ref, + backlinkClass: Ref> +): Promise { + const attachedDocId = doc._id + + const backlinks: Data[] = [] + const attributes = control.hierarchy.getAllAttributes(doc._class) + for (const attr of attributes.values()) { + if (isMarkupType(attr.type._class)) { + const content = (doc as any)[attr.name]?.toString() ?? '' + const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content) + backlinks.push(...attrBacklinks) + } else if (attr.type._class === core.class.TypeCollaborativeDoc) { + const collaborativeDoc = (doc as any)[attr.name] as CollaborativeDoc + try { + const ydoc = await loadCollaborativeDoc(storage, control.workspace, collaborativeDoc, control.ctx) + if (ydoc !== undefined) { + const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, yDocToBuffer(ydoc)) + backlinks.push(...attrBacklinks) + } + } catch { + // do nothing, the collaborative doc does not sem to exist yet + } + } + } + + return getBacklinksTxes(txFactory, backlinks, []) +} + +export async function getUpdateBacklinksTxes ( + control: TriggerControl, + storage: StorageAdapter, + txFactory: TxFactory, + doc: Doc, + backlinkId: Ref, + backlinkClass: Ref> +): Promise { + const attachedDocId = doc._id + + // collect attribute backlinks + let hasBacklinkAttrs = false + const backlinks: Data[] = [] + const attributes = control.hierarchy.getAllAttributes(doc._class) + for (const attr of attributes.values()) { + if (isMarkupType(attr.type._class)) { + hasBacklinkAttrs = true + const content = (doc as any)[attr.name]?.toString() ?? '' + const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content) + backlinks.push(...attrBacklinks) + } else if (attr.type._class === core.class.TypeCollaborativeDoc) { + hasBacklinkAttrs = true + try { + const collaborativeDoc = (doc as any)[attr.name] as CollaborativeDoc + const ydoc = await loadCollaborativeDoc(storage, control.workspace, collaborativeDoc, control.ctx) + if (ydoc !== undefined) { + const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, yDocToBuffer(ydoc)) + backlinks.push(...attrBacklinks) + } + } catch { + // do nothing, the collaborative doc does not sem to exist yet + } + } + } + + // There is a chance that backlinks are managed manually + // do not update backlinks if there are no backlink sources in the doc + if (hasBacklinkAttrs) { + const current = await control.findAll(chunter.class.Backlink, { + backlinkId, + backlinkClass, + attachedDocId, + collection: 'backlinks' + }) + + return getBacklinksTxes(txFactory, backlinks, current) + } + + return [] +} + export function getBacklinks ( backlinkId: Ref, backlinkClass: Ref>, attachedDocId: Ref | undefined, - content: string + content: string | Buffer ): Array> { - const doc = parseHTML(content, extensions) - const result: Array> = [] - const references = extractReferences(doc) + const references = [] + + if (content instanceof Buffer) { + const nodes = yDocContentToNodes(extensions, content) + for (const node of nodes) { + references.push(...extractReferences(node)) + } + } else { + const doc = parseHTML(content, extensions) + references.push(...extractReferences(doc)) + } for (const ref of references) { if (ref.objectId !== attachedDocId && ref.objectId !== backlinkId) { result.push({ @@ -117,66 +220,6 @@ export function getBacklinksTxes (txFactory: TxFactory, backlinks: Data, - backlinkClass: Ref> -): Tx[] { - const attachedDocId = doc._id - - const backlinks: Data[] = [] - const attributes = control.hierarchy.getAllAttributes(doc._class) - for (const attr of attributes.values()) { - if (attr.type._class === core.class.TypeMarkup) { - const content = (doc as any)[attr.name]?.toString() ?? '' - const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content) - backlinks.push(...attrBacklinks) - } - } - - return getBacklinksTxes(txFactory, backlinks, []) -} - -export async function getUpdateBacklinksTxes ( - control: TriggerControl, - txFactory: TxFactory, - doc: Doc, - backlinkId: Ref, - backlinkClass: Ref> -): Promise { - const attachedDocId = doc._id - - // collect attribute backlinks - let hasBacklinkAttrs = false - const backlinks: Data[] = [] - const attributes = control.hierarchy.getAllAttributes(doc._class) - for (const attr of attributes.values()) { - if (attr.type._class === core.class.TypeMarkup) { - hasBacklinkAttrs = true - const content = (doc as any)[attr.name]?.toString() ?? '' - const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content) - backlinks.push(...attrBacklinks) - } - } - - // There is a chance that backlinks are managed manually - // do not update backlinks if there are no backlink sources in the doc - if (hasBacklinkAttrs) { - const current = await control.findAll(chunter.class.Backlink, { - backlinkId, - backlinkClass, - attachedDocId, - collection: 'backlinks' - }) - - return getBacklinksTxes(txFactory, backlinks, current) - } - - return [] -} - export async function getRemoveBacklinksTxes ( control: TriggerControl, txFactory: TxFactory, diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index 7e3840ac9c..75e004121d 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -27,7 +27,6 @@ import core, { AttachedDoc, Class, concatLink, - Data, Doc, DocumentQuery, FindOptions, @@ -41,8 +40,7 @@ import core, { TxFactory, TxProcessor, TxRemoveDoc, - TxUpdateDoc, - Type + TxUpdateDoc } from '@hcengineering/core' import notification, { NotificationContent } from '@hcengineering/notification' import { getMetadata, IntlString } from '@hcengineering/platform' @@ -57,74 +55,15 @@ import { stripTags } from '@hcengineering/text' import { Person, PersonAccount } from '@hcengineering/contact' import activity, { ActivityMessage } from '@hcengineering/activity' +import { + getCreateBacklinksTxes, + getRemoveBacklinksTxes, + getUpdateBacklinksTxes, + guessBacklinkTx, + isMarkupType, + isCollaborativeType +} from './backlinks' import { IsChannelMessage, IsDirectMessage, IsMeMentioned, IsThreadMessage } from './utils' -import { getBacklinks, getBacklinksTxes, getRemoveBacklinksTxes, guessBacklinkTx } from './backlinks' - -export { getBacklinksTxes } from './backlinks' - -function isMarkupType (type: Ref>>): boolean { - return type === core.class.TypeMarkup || type === core.class.TypeCollaborativeMarkup -} - -function getCreateBacklinksTxes ( - control: TriggerControl, - txFactory: TxFactory, - doc: Doc, - backlinkId: Ref, - backlinkClass: Ref> -): Tx[] { - const attachedDocId = doc._id - - const backlinks: Data[] = [] - const attributes = control.hierarchy.getAllAttributes(doc._class) - for (const attr of attributes.values()) { - if (isMarkupType(attr.type._class)) { - const content = (doc as any)[attr.name]?.toString() ?? '' - const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content) - backlinks.push(...attrBacklinks) - } - } - - return getBacklinksTxes(txFactory, backlinks, []) -} - -async function getUpdateBacklinksTxes ( - control: TriggerControl, - txFactory: TxFactory, - doc: Doc, - backlinkId: Ref, - backlinkClass: Ref> -): Promise { - const attachedDocId = doc._id - - // collect attribute backlinks - let hasBacklinkAttrs = false - const backlinks: Data[] = [] - const attributes = control.hierarchy.getAllAttributes(doc._class) - for (const attr of attributes.values()) { - if (isMarkupType(attr.type._class)) { - hasBacklinkAttrs = true - const content = (doc as any)[attr.name]?.toString() ?? '' - const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content) - backlinks.push(...attrBacklinks) - } - } - - // There is a chance that backlinks are managed manually - // do not update backlinks if there are no backlink sources in the doc - if (hasBacklinkAttrs) { - const current = await control.findAll(chunter.class.Backlink, { - backlinkId, - backlinkClass, - attachedDocId, - collection: 'backlinks' - }) - - return getBacklinksTxes(txFactory, backlinks, current) - } - - return [] -} /** * @public @@ -321,15 +260,24 @@ async function BacklinksCreate (tx: Tx, control: TriggerControl): Promise if (ctx._class !== core.class.TxCreateDoc) return [] if (control.hierarchy.isDerived(ctx.objectClass, chunter.class.Backlink)) return [] - const txFactory = new TxFactory(control.txFactory.account) + control.storageFx(async (adapter) => { + const txFactory = new TxFactory(control.txFactory.account) - const doc = TxProcessor.createDoc2Doc(ctx) - const targetTx = guessBacklinkTx(control.hierarchy, tx as TxCUD) - const txes: Tx[] = getCreateBacklinksTxes(control, txFactory, doc, targetTx.objectId, targetTx.objectClass) + const doc = TxProcessor.createDoc2Doc(ctx) + const targetTx = guessBacklinkTx(control.hierarchy, tx as TxCUD) + const txes: Tx[] = await getCreateBacklinksTxes( + control, + adapter, + txFactory, + doc, + targetTx.objectId, + targetTx.objectClass + ) - if (txes.length !== 0) { - await control.apply(txes, true) - } + if (txes.length !== 0) { + await control.apply(txes, true) + } + }) return [] } @@ -340,9 +288,11 @@ async function BacklinksUpdate (tx: Tx, control: TriggerControl): Promise let hasUpdates = false const attributes = control.hierarchy.getAllAttributes(ctx.objectClass) for (const attr of attributes.values()) { - if (isMarkupType(attr.type._class) && attr.name in ctx.operations) { - hasUpdates = true - break + if (isMarkupType(attr.type._class) || isCollaborativeType(attr.type._class)) { + if (TxProcessor.txHasUpdate(ctx, attr.name)) { + hasUpdates = true + break + } } } @@ -350,15 +300,23 @@ async function BacklinksUpdate (tx: Tx, control: TriggerControl): Promise const rawDoc = (await control.findAll(ctx.objectClass, { _id: ctx.objectId }))[0] if (rawDoc !== undefined) { - const txFactory = new TxFactory(control.txFactory.account) + control.storageFx(async (adapter) => { + const txFactory = new TxFactory(control.txFactory.account) + const doc = TxProcessor.updateDoc2Doc(rawDoc, ctx) + const targetTx = guessBacklinkTx(control.hierarchy, tx as TxCUD) + const txes: Tx[] = await getUpdateBacklinksTxes( + control, + adapter, + txFactory, + doc, + targetTx.objectId, + targetTx.objectClass + ) - const doc = TxProcessor.updateDoc2Doc(rawDoc, ctx) - const targetTx = guessBacklinkTx(control.hierarchy, tx as TxCUD) - const txes: Tx[] = await getUpdateBacklinksTxes(control, txFactory, doc, targetTx.objectId, targetTx.objectClass) - - if (txes.length !== 0) { - await control.apply(txes, true) - } + if (txes.length !== 0) { + await control.apply(txes, true) + } + }) } } diff --git a/server-plugins/collaboration-resources/.eslintrc.js b/server-plugins/collaboration-resources/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/server-plugins/collaboration-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/server-plugins/collaboration-resources/.npmignore b/server-plugins/collaboration-resources/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/server-plugins/collaboration-resources/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/server-plugins/collaboration-resources/config/rig.json b/server-plugins/collaboration-resources/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/server-plugins/collaboration-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/server-plugins/collaboration-resources/jest.config.js b/server-plugins/collaboration-resources/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/server-plugins/collaboration-resources/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/server-plugins/collaboration-resources/package.json b/server-plugins/collaboration-resources/package.json new file mode 100644 index 0000000000..3b329aaac7 --- /dev/null +++ b/server-plugins/collaboration-resources/package.json @@ -0,0 +1,38 @@ +{ + "name": "@hcengineering/server-collaboration-resources", + "version": "0.6.0", + "main": "lib/index.js", + "author": "Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent", + "_phase:build": "compile", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.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.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "prettier-plugin-svelte": "^3.1.0" + }, + "dependencies": { + "@hcengineering/core": "^0.6.28", + "@hcengineering/platform": "^0.6.9", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/collaboration": "^0.6.0" + } +} diff --git a/server-plugins/collaboration-resources/src/index.ts b/server-plugins/collaboration-resources/src/index.ts new file mode 100644 index 0000000000..18c945ecf0 --- /dev/null +++ b/server-plugins/collaboration-resources/src/index.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 type { CollaborativeDoc, Doc, Tx, TxRemoveDoc } from '@hcengineering/core' +import core, { TxProcessor } from '@hcengineering/core' +import { removeCollaborativeDoc } from '@hcengineering/collaboration' +import type { TriggerControl } from '@hcengineering/server-core' + +/** + * @public + */ +export async function OnDelete (tx: Tx, { hierarchy, storageFx, removedMap, ctx }: TriggerControl): Promise { + const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc + + if (rmTx._class !== core.class.TxRemoveDoc) { + return [] + } + + // Obtain document being deleted + const doc = removedMap.get(rmTx.objectId) + + // Ids of files to delete from storage + const toDelete: CollaborativeDoc[] = [] + + const attributes = hierarchy.getAllAttributes(rmTx.objectClass) + for (const attribute of attributes.values()) { + if (hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeDoc)) { + const value = (doc as any)[attribute.name] as CollaborativeDoc + if (value !== undefined) { + toDelete.push(value) + } + } + } + + storageFx(async (adapter, bucket) => { + // TODO This is not accurate way to delete collaborative document + // Even though we are deleting it here, the document can be currently in use by someone else + // and when editing session ends, the collborator service will recreate the document again + await removeCollaborativeDoc(adapter, bucket, toDelete, ctx) + }) + + return [] +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export default async () => ({ + trigger: { + OnDelete + } +}) diff --git a/server-plugins/collaboration-resources/tsconfig.json b/server-plugins/collaboration-resources/tsconfig.json new file mode 100644 index 0000000000..4e0fa3fe7f --- /dev/null +++ b/server-plugins/collaboration-resources/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/server-plugins/collaboration/.eslintrc.js b/server-plugins/collaboration/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/server-plugins/collaboration/.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/server-plugins/collaboration/.npmignore b/server-plugins/collaboration/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/server-plugins/collaboration/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/server-plugins/collaboration/config/rig.json b/server-plugins/collaboration/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/server-plugins/collaboration/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/server-plugins/collaboration/jest.config.js b/server-plugins/collaboration/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/server-plugins/collaboration/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/server-plugins/collaboration/package.json b/server-plugins/collaboration/package.json new file mode 100644 index 0000000000..3cee01be10 --- /dev/null +++ b/server-plugins/collaboration/package.json @@ -0,0 +1,38 @@ +{ + "name": "@hcengineering/server-collaboration", + "version": "0.6.0", + "main": "lib/index.js", + "author": "Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent", + "_phase:build": "compile", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@types/node": "~20.11.16", + "@typescript-eslint/eslint-plugin": "^6.11.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.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "prettier-plugin-svelte": "^3.1.0" + }, + "dependencies": { + "@hcengineering/core": "^0.6.28", + "@hcengineering/platform": "^0.6.9", + "@hcengineering/server-core": "^0.6.1" + } +} diff --git a/server-plugins/collaboration/src/fulltext.ts b/server-plugins/collaboration/src/fulltext.ts new file mode 100644 index 0000000000..c13b439159 --- /dev/null +++ b/server-plugins/collaboration/src/fulltext.ts @@ -0,0 +1,173 @@ +// +// 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, + CollaborativeDoc, + Doc, + DocIndexState, + DocumentQuery, + DocumentUpdate, + MeasureContext, + Ref, + WorkspaceId, + parseCollaborativeDoc +} from '@hcengineering/core' +import { + ContentTextAdapter, + DbAdapter, + DocUpdateHandler, + FullTextPipeline, + FullTextPipelineStage, + IndexedDoc, + StorageAdapter, + contentStageId, + docKey, + docUpdKey, + fieldStateId, + getFullTextIndexableAttributes +} from '@hcengineering/server-core' + +/** + * @public + */ +export class CollaborativeContentRetrievalStage implements FullTextPipelineStage { + require = [] + stageId = contentStageId + + extra = ['content', 'base64'] + digest = '^digest' + + enabled = true + + // Clear all except following. + clearExcept: string[] = [fieldStateId, contentStageId] + + updateFields: DocUpdateHandler[] = [] + + textLimit = 100 * 1024 + + stageValue: boolean | string = true + + constructor ( + readonly storageAdapter: StorageAdapter | undefined, + readonly workspace: WorkspaceId, + readonly metrics: MeasureContext, + private readonly contentAdapter: ContentTextAdapter + ) {} + + async initialize (ctx: MeasureContext, storage: DbAdapter, pipeline: FullTextPipeline): Promise { + // Just do nothing + } + + async search ( + _classes: Ref>[], + search: DocumentQuery, + size?: number, + from?: number + ): Promise<{ docs: IndexedDoc[], pass: boolean }> { + return { docs: [], pass: true } + } + + async collect (toIndex: DocIndexState[], pipeline: FullTextPipeline): Promise { + for (const doc of toIndex) { + if (pipeline.cancelling) { + return + } + await this.updateContent(doc, pipeline) + } + } + + async updateContent (doc: DocIndexState, pipeline: FullTextPipeline): Promise { + const attributes = getFullTextIndexableAttributes(pipeline.hierarchy, doc.objectClass) + // Copy content attributes as well. + const update: DocumentUpdate = {} + + if (pipeline.cancelling) { + return + } + + try { + for (const [, val] of Object.entries(attributes)) { + if (val.type._class === core.class.TypeCollaborativeDoc) { + const collaborativeDoc = doc.attributes[docKey(val.name, { _class: val.attributeOf })] as CollaborativeDoc + if (collaborativeDoc !== undefined && collaborativeDoc !== '') { + const { documentId } = parseCollaborativeDoc(collaborativeDoc) + + let docInfo: any | undefined + try { + docInfo = await this.storageAdapter?.stat(this.workspace, documentId) + } catch (err: any) { + // not found. + } + + if (docInfo !== undefined) { + const digest = docInfo.etag + const digestKey = docKey(val.name + this.digest, { _class: val.attributeOf }) + if (doc.attributes[digestKey] !== digest) { + ;(update as any)[docUpdKey(digestKey)] = digest + + const contentType = ((docInfo.metaData['content-type'] as string) ?? '').split(';')[0] + const readable = await this.storageAdapter?.get(this.workspace, documentId) + + if (readable !== undefined) { + let textContent = await this.metrics.with( + 'fetch', + {}, + async () => await this.contentAdapter.content(documentId, contentType, readable) + ) + readable?.destroy() + + textContent = textContent + .split(/ +|\t+|\f+/) + .filter((it) => it) + .join(' ') + .split(/\n\n+/) + .join('\n') + + // trim to large content + if (textContent.length > this.textLimit) { + textContent = textContent.slice(0, this.textLimit) + } + textContent = Buffer.from(textContent).toString('base64') + ;(update as any)[docUpdKey(val.name, { _class: val.attributeOf, extra: this.extra })] = textContent + } + } + } + } + } + } + } catch (err: any) { + const wasError = (doc as any).error !== undefined + + await pipeline.update(doc._id, false, { [docKey('error')]: JSON.stringify({ message: err.message, err }) }) + if (wasError) { + return + } + // Print error only first time, and update it in doc index + console.error(err) + return + } + + await pipeline.update(doc._id, true, update) + } + + async remove (docs: DocIndexState[], pipeline: FullTextPipeline): Promise { + // will be handled by field processor + for (const doc of docs) { + await pipeline.update(doc._id, true, {}) + } + } +} diff --git a/server-plugins/collaboration/src/index.ts b/server-plugins/collaboration/src/index.ts new file mode 100644 index 0000000000..a98c5b3620 --- /dev/null +++ b/server-plugins/collaboration/src/index.ts @@ -0,0 +1,34 @@ +// +// 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 { Plugin, Resource } from '@hcengineering/platform' +import { plugin } from '@hcengineering/platform' +import type { TriggerFunc } from '@hcengineering/server-core' + +export * from './fulltext' + +/** + * @public + */ +export const serverCollaborationId = 'server-collaboration' as Plugin + +/** + * @public + */ +export default plugin(serverCollaborationId, { + trigger: { + OnDelete: '' as Resource + } +}) diff --git a/server-plugins/collaboration/tsconfig.json b/server-plugins/collaboration/tsconfig.json new file mode 100644 index 0000000000..4e0fa3fe7f --- /dev/null +++ b/server-plugins/collaboration/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/server/collaboration/.eslintrc.js b/server/collaboration/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/server/collaboration/.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/server/collaboration/.npmignore b/server/collaboration/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/server/collaboration/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/server/collaboration/config/rig.json b/server/collaboration/config/rig.json new file mode 100644 index 0000000000..06a2a2e17a --- /dev/null +++ b/server/collaboration/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/server/collaboration/jest.config.js b/server/collaboration/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/server/collaboration/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/server/collaboration/package.json b/server/collaboration/package.json new file mode 100644 index 0000000000..27fd112862 --- /dev/null +++ b/server/collaboration/package.json @@ -0,0 +1,39 @@ +{ + "name": "@hcengineering/collaboration", + "version": "0.6.0", + "main": "lib/index.js", + "author": "Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent", + "_phase:build": "compile", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@types/node": "~20.11.16", + "@typescript-eslint/eslint-plugin": "^6.11.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.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "prettier-plugin-svelte": "^3.1.0" + }, + "dependencies": { + "@hcengineering/core": "^0.6.28", + "@hcengineering/minio": "^0.6.0", + "base64-js": "^1.5.1", + "yjs": "^13.5.52" + } +} diff --git a/server/collaboration/src/history/__tests__/branch.test.ts b/server/collaboration/src/history/__tests__/branch.test.ts new file mode 100644 index 0000000000..e798ee1e80 --- /dev/null +++ b/server/collaboration/src/history/__tests__/branch.test.ts @@ -0,0 +1,132 @@ +// +// 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 { Doc as YDoc, encodeStateAsUpdate, encodeStateVector } from 'yjs' + +import { yDocBranch, yDocBranchWithGC } from '../branch' + +describe('branch', () => { + describe('yDocBranch', () => { + it('branches document without gc', async () => { + const source = new YDoc({ gc: false }) + + applyGarbageCollectableChanges(source) + + const target = yDocBranch(source) + + expect(target.gc).toBeFalsy() + + // ensure target data + const sourceData = source.getArray('data') + const targetData = target.getArray('data') + expect(targetData.toArray()).toEqual(expect.arrayContaining(sourceData.toArray())) + + // ensure target state + const sourceState = encodeStateVector(source) + const targetState = encodeStateVector(target) + expect(targetState).toEqual(sourceState) + + // ensure target updates the same as source regardless of gc + const sourceUpdate = encodeStateAsUpdate(source) + const targetUpdate = encodeStateAsUpdate(target) + expect(targetUpdate).toEqual(sourceUpdate) + }) + + it('branches document state with gc', async () => { + const source = new YDoc({ gc: true }) + + applyGarbageCollectableChanges(source) + + const target = yDocBranch(source) + + expect(target.gc).toBeTruthy() + + // ensure target data + const sourceData = source.getArray('data') + const targetData = target.getArray('data') + expect(targetData.toArray()).toEqual(expect.arrayContaining(sourceData.toArray())) + + // ensure target state + const sourceState = encodeStateVector(source) + const targetState = encodeStateVector(target) + expect(targetState).toEqual(sourceState) + + // ensure target updates the same as source regardless of gc + const sourceUpdate = encodeStateAsUpdate(source) + const targetUpdate = encodeStateAsUpdate(target) + expect(targetUpdate).toEqual(sourceUpdate) + }) + }) + + describe('yDocBranchWithGC', () => { + it('branches document state without gc', async () => { + const source = new YDoc({ gc: false }) + + applyGarbageCollectableChanges(source) + + const target = yDocBranchWithGC(source) + + expect(target.gc).toBeFalsy() + + // ensure target data + const sourceData = source.getArray('data') + const targetData = target.getArray('data') + expect(targetData.toArray()).toEqual(expect.arrayContaining(sourceData.toArray())) + + // ensure target state + const sourceState = encodeStateVector(source) + const targetState = encodeStateVector(target) + expect(targetState).toEqual(sourceState) + + // ensure target updates different because source is not gc-ed + const sourceUpdate = encodeStateAsUpdate(source) + const targetUpdate = encodeStateAsUpdate(target) + expect(targetUpdate).not.toEqual(sourceUpdate) + }) + + it('branches document state with gc', async () => { + const source = new YDoc({ gc: true }) + + applyGarbageCollectableChanges(source) + + const target = yDocBranchWithGC(source) + + expect(target.gc).toBeTruthy() + + // ensure target data + const sourceData = source.getArray('data') + const targetData = target.getArray('data') + expect(targetData.toArray()).toEqual(expect.arrayContaining(sourceData.toArray())) + + // ensure target state + const sourceState = encodeStateVector(source) + const targetState = encodeStateVector(target) + expect(targetState).toEqual(sourceState) + + // ensure target updates the same because source is gc-ed + const sourceUpdate = encodeStateAsUpdate(source) + const targetUpdate = encodeStateAsUpdate(target) + expect(targetUpdate).toEqual(sourceUpdate) + }) + }) + + function applyGarbageCollectableChanges (ydoc: YDoc): void { + const sourceData = ydoc.getArray('data') + sourceData.insert(0, ['a']) + sourceData.insert(1, [1, 2]) + sourceData.delete(0, 1) + sourceData.insert(2, [3]) + } +}) diff --git a/server/collaboration/src/history/__tests__/history.test.ts b/server/collaboration/src/history/__tests__/history.test.ts new file mode 100644 index 0000000000..f78e385210 --- /dev/null +++ b/server/collaboration/src/history/__tests__/history.test.ts @@ -0,0 +1,159 @@ +// +// 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 { generateId } from '@hcengineering/core' +import { Doc as YDoc, encodeStateAsUpdate } from 'yjs' + +import { YDocVersion, addVersion, deleteVersion, getVersion, getVersionData, listVersions } from '../history' + +const HISTORY = 'history' +const UPDATES = 'updates' + +describe('history', () => { + let ydoc: YDoc + + beforeEach(() => { + ydoc = new YDoc() + }) + + it('addVersion should append new version', async () => { + const versionId = generateId() + const version = yDocVersion(versionId) + const update = encodeStateAsUpdate(ydoc) + + addVersion(ydoc, version, update) + + const history = ydoc.getArray(HISTORY) + const updates = ydoc.getMap(UPDATES) + + expect(history.length).toEqual(1) + expect(updates.size).toEqual(1) + + expect(history.get(0)).toEqual(version) + expect(updates.get(versionId)).toBeDefined() + }) + + it('addVersion should raise an error when a version already exists', async () => { + const versionId = generateId() + const version = yDocVersion(versionId) + const update = encodeStateAsUpdate(ydoc) + + addVersion(ydoc, version, update) + expect(() => { + addVersion(ydoc, version, update) + }).toThrow() + + const history = ydoc.getArray(HISTORY) + const updates = ydoc.getMap(UPDATES) + + expect(history.length).toEqual(1) + expect(updates.size).toEqual(1) + + expect(history.get(0)).toEqual(version) + expect(updates.get(versionId)).toBeDefined() + }) + + it('getVersion should get existing version data', async () => { + const versionId = generateId() + const version = yDocVersion(versionId) + const update = encodeStateAsUpdate(ydoc) + + addVersion(ydoc, yDocVersion(generateId()), encodeStateAsUpdate(ydoc)) + addVersion(ydoc, yDocVersion(generateId()), encodeStateAsUpdate(ydoc)) + addVersion(ydoc, version, update) + + const history = ydoc.getArray(HISTORY) + const updates = ydoc.getMap(UPDATES) + + expect(history.length).toEqual(3) + expect(updates.size).toEqual(3) + + expect(getVersion(ydoc, versionId)).toEqual(version) + }) + + it('getVersion should return undefined for unknown version', async () => { + const versionId = generateId() + const version = yDocVersion(versionId) + addVersion(ydoc, version, encodeStateAsUpdate(ydoc)) + + expect(getVersion(ydoc, generateId())).toBeUndefined() + }) + + it('listVersions should return existing versions', async () => { + const version1 = yDocVersion(generateId()) + const version2 = yDocVersion(generateId()) + + addVersion(ydoc, version1, encodeStateAsUpdate(ydoc)) + addVersion(ydoc, version2, encodeStateAsUpdate(ydoc)) + + expect(listVersions(ydoc)).toEqual(expect.arrayContaining([version1, version2])) + }) + + it('listVersions should return empty list when no versions', async () => { + expect(listVersions(ydoc)).toEqual([]) + }) + + it('getVersionData should get existing version data', async () => { + const versionId = generateId() + const version = yDocVersion(versionId) + const update = encodeStateAsUpdate(ydoc) + + addVersion(ydoc, version, update) + addVersion(ydoc, yDocVersion(generateId()), encodeStateAsUpdate(ydoc)) + addVersion(ydoc, yDocVersion(generateId()), encodeStateAsUpdate(ydoc)) + + const history = ydoc.getArray(HISTORY) + const updates = ydoc.getMap(UPDATES) + + expect(history.length).toEqual(3) + expect(updates.size).toEqual(3) + + expect(getVersionData(ydoc, versionId)).toEqual(update) + }) + + it('getVersionData should return undefined for unknown version', async () => { + const versionId = generateId() + const version = yDocVersion(versionId) + addVersion(ydoc, version, encodeStateAsUpdate(ydoc)) + + expect(getVersionData(ydoc, generateId())).toBeUndefined() + }) + + it('deleteVersion should delete existing version', async () => { + const versionId = generateId() + const version = yDocVersion(versionId) + addVersion(ydoc, version, encodeStateAsUpdate(ydoc)) + + deleteVersion(ydoc, versionId) + + const history = ydoc.getArray(HISTORY) + const updates = ydoc.getMap(UPDATES) + + expect(history.length).toEqual(0) + expect(updates.size).toEqual(0) + + expect(getVersion(ydoc, versionId)).toEqual(undefined) + expect(getVersionData(ydoc, versionId)).toEqual(undefined) + }) +}) + +function yDocVersion (versionId: string): YDocVersion { + return { + versionId, + name: versionId, + createdBy: 'unit test', + createdOn: Date.now() + } +} diff --git a/server/collaboration/src/history/__tests__/snapshot.test.ts b/server/collaboration/src/history/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..9f71db68d4 --- /dev/null +++ b/server/collaboration/src/history/__tests__/snapshot.test.ts @@ -0,0 +1,109 @@ +// +// 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 { generateId } from '@hcengineering/core' +import { Doc as YDoc } from 'yjs' + +import { createYdocSnapshot, restoreYdocSnapshot } from '../snapshot' +import { YDocVersion } from '../history' + +const HISTORY = 'history' +const UPDATES = 'updates' + +describe('snapshot', () => { + let yContent: YDoc + let yHistory: YDoc + + beforeEach(() => { + yContent = new YDoc({ gc: false }) + yHistory = new YDoc() + }) + + it('createYdocSnapshot appends new version', async () => { + const versionId = generateId() + const version = yDocVersion(versionId) + + createYdocSnapshot(yContent, yHistory, version) + + const history = yHistory.getArray(HISTORY) + const updates = yHistory.getMap(UPDATES) + + expect(history.length).toEqual(1) + expect(updates.size).toEqual(1) + + expect(history.get(0)).toEqual(version) + expect(updates.get(versionId)).toBeDefined() + }) + + it('restoreYdocSnapshot restores existing version', async () => { + const versionId = generateId() + const version = yDocVersion(versionId) + + const data = yContent.getArray('data') + data.insert(0, [1, 2, 3]) + expect(data.toArray()).toEqual(expect.arrayContaining([1, 2, 3])) + + createYdocSnapshot(yContent, yHistory, version) + + data.delete(1, 1) + expect(data.toArray()).toEqual(expect.arrayContaining([1, 3])) + + const yRestore = restoreYdocSnapshot(yContent, yHistory, versionId) + + // assert the restored doc has not been changed + expect(yRestore).toBeDefined() + expect(yRestore?.getArray('data').toArray()).toEqual(expect.arrayContaining([1, 2, 3])) + + // assert the original doc has not been changed + expect(yContent.getArray('data').toArray()).toEqual(expect.arrayContaining([1, 3])) + }) + + it('restoreYdocSnapshot throws an error when gc is enabled', async () => { + const versionId = generateId() + const version = yDocVersion(versionId) + + yContent = new YDoc({ gc: true }) + createYdocSnapshot(yContent, yHistory, version) + expect(() => restoreYdocSnapshot(yContent, yHistory, versionId)).toThrow() + }) + + it('restoreYdocSnapshot does not restore version that does not exist', async () => { + const versionId = generateId() + + const yRestore = restoreYdocSnapshot(yContent, yHistory, versionId) + expect(yRestore).toBeUndefined() + }) + + it('restoreYdocSnapshot restored document has gc enabled', async () => { + const versionId = generateId() + const version = yDocVersion(versionId) + + createYdocSnapshot(yContent, yHistory, version) + const yRestore = restoreYdocSnapshot(yContent, yHistory, versionId) + + // so far we don't care whether gc is enabled or not in the restore + // but we need to ensure we understand that it is enabled + expect(yRestore?.gc).toEqual(true) + }) +}) + +function yDocVersion (versionId: string): YDocVersion { + return { + versionId, + name: versionId, + createdBy: 'unit test', + createdOn: Date.now() + } +} diff --git a/server/collaboration/src/history/branch.ts b/server/collaboration/src/history/branch.ts new file mode 100644 index 0000000000..7f54aed529 --- /dev/null +++ b/server/collaboration/src/history/branch.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 { Doc as YDoc, applyUpdate, encodeStateAsUpdate } from 'yjs' + +/** + * Branch (copy) document content as is. + * + * If the source document has gc parameter enabled, then garbage + * collection will be performed. The result document will have the same + * gc parameter value as the source document. + * + * @public + * */ +export function yDocBranch (source: YDoc): YDoc { + const target = new YDoc({ gc: source.gc }) + + const update = encodeStateAsUpdate(source) + applyUpdate(target, update) + + return target +} + +/** + * Branch (copy) document content with garbage collecting while applying update. + * + * Garbage collection will be performed regardless of the gc parameter + * in the source document. The result document will have the same gc + * parameter value as the source document. + * + * @public + * */ +export function yDocBranchWithGC (source: YDoc): YDoc { + const target = new YDoc({ gc: source.gc }) + + const gc = new YDoc({ gc: true }) + applyUpdate(gc, encodeStateAsUpdate(source)) + applyUpdate(target, encodeStateAsUpdate(gc)) + + return target +} diff --git a/server/collaboration/src/history/history.ts b/server/collaboration/src/history/history.ts new file mode 100644 index 0000000000..51762179d7 --- /dev/null +++ b/server/collaboration/src/history/history.ts @@ -0,0 +1,111 @@ +// +// 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 { Timestamp } from '@hcengineering/core' +import { fromByteArray, toByteArray } from 'base64-js' +import { Doc as YDoc } from 'yjs' +import { YArray, YMap } from 'yjs/dist/src/internals' + +/** + * This module provides utils for document version storage based on YDoc + * + * At the top level the history document contains two fields: + * 1. history + * An array containing version ids in creation order + * 2. updates + * A map containing version data keyed by version id + * + * { + * "history": [ + * { "versionId": "version1", ... }, + * { "versionId": "version2", ... }, + * ... + * ], + * "updates": { + * "version1": ... version data as ydoc update base64 encoded ..., + * "version2": ... version data as ydoc update base64 encoded ..., + * ... + * } + * } + */ + +/** @public */ +export interface YDocVersion { + versionId: string + name: string + + createdBy: string + createdOn: Timestamp +} + +const HISTORY = 'history' +const UPDATES = 'updates' + +function getHistory (ydoc: YDoc): YArray { + return ydoc.getArray(HISTORY) +} + +function getUpdates (ydoc: YDoc): YMap { + return ydoc.getMap(UPDATES) +} + +/** @public */ +export function addVersion (ydoc: YDoc, version: YDocVersion, update: Uint8Array): void { + const history = getHistory(ydoc) + const updates = getUpdates(ydoc) + + const { versionId } = version + + if (updates.has(versionId)) { + throw Error('history item already exists') + } + + ydoc.transact((tr) => { + history.push([version]) + updates.set(versionId, fromByteArray(update)) + }) +} + +/** @public */ +export function getVersion (ydoc: YDoc, versionId: string): YDocVersion | undefined { + const history = getHistory(ydoc) + return history.toArray().find((p) => p.versionId === versionId) +} + +/** @public */ +export function listVersions (ydoc: YDoc): YDocVersion[] { + return getHistory(ydoc).toArray() +} + +/** @public */ +export function getVersionData (ydoc: YDoc, versionId: string): Uint8Array | undefined { + const updates = getUpdates(ydoc) + const update = updates.get(versionId) + return update !== undefined ? toByteArray(update) : undefined +} + +/** @public */ +export function deleteVersion (ydoc: YDoc, versionId: string): void { + const history = getHistory(ydoc) + const updates = getUpdates(ydoc) + + ydoc.transact((tr) => { + const index = history.toArray().findIndex((p) => p.versionId === versionId) + if (index !== -1) { + history.delete(index, 1) + } + updates.delete(versionId) + }) +} diff --git a/server/collaboration/src/history/snapshot.ts b/server/collaboration/src/history/snapshot.ts new file mode 100644 index 0000000000..ac0d273af3 --- /dev/null +++ b/server/collaboration/src/history/snapshot.ts @@ -0,0 +1,36 @@ +// +// 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 { Doc as YDoc } from 'yjs' +import * as Y from 'yjs' + +import { YDocVersion, addVersion, getVersionData } from './history' + +/** @public */ +export function createYdocSnapshot (yContent: YDoc, yHistory: YDoc, version: YDocVersion): void { + const snapshot = Y.snapshot(yContent) + const update = Y.encodeSnapshot(snapshot) + + addVersion(yHistory, version, update) +} + +/** @public */ +export function restoreYdocSnapshot (yContent: YDoc, yHistory: YDoc, versionId: string): YDoc | undefined { + const update = getVersionData(yHistory, versionId) + if (update !== undefined) { + const snapshot = Y.decodeSnapshot(update) + return Y.createDocFromSnapshot(yContent, snapshot) + } +} diff --git a/server/collaboration/src/index.ts b/server/collaboration/src/index.ts new file mode 100644 index 0000000000..5999004357 --- /dev/null +++ b/server/collaboration/src/index.ts @@ -0,0 +1,21 @@ +// +// 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 './history/branch' +export * from './history/history' +export * from './history/snapshot' +export * from './utils/collaborative-doc' +export * from './utils/minio' +export * from './utils/ydoc' diff --git a/server/collaboration/src/uri.ts b/server/collaboration/src/uri.ts new file mode 100644 index 0000000000..f8dec9003f --- /dev/null +++ b/server/collaboration/src/uri.ts @@ -0,0 +1,41 @@ +// +// 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, Domain, Ref, parseCollaborativeDoc } from '@hcengineering/core' + +export type DocumentURI = string & { __documentUri: true } + +export function collaborativeDocumentUri (workspaceUrl: string, docId: CollaborativeDoc): DocumentURI { + const { documentId, versionId } = parseCollaborativeDoc(docId) + return `minio://${workspaceUrl}/${documentId}/${versionId}` as DocumentURI +} + +export function platformDocumentUri ( + workspaceUrl: string, + objectClass: Ref>, + objectId: Ref, + objectAttr: string +): DocumentURI { + return `platform://${workspaceUrl}/${objectClass}/${objectId}/${objectAttr}` as DocumentURI +} + +export function mongodbDocumentUri ( + workspaceUrl: string, + domain: Domain, + docId: Ref, + objectAttr: string +): DocumentURI { + return `mongodb://${workspaceUrl}/${domain}/${docId}/${objectAttr}` as DocumentURI +} diff --git a/server/collaboration/src/utils/__tests__/collaborative-doc.test.ts b/server/collaboration/src/utils/__tests__/collaborative-doc.test.ts new file mode 100644 index 0000000000..1cd848b342 --- /dev/null +++ b/server/collaboration/src/utils/__tests__/collaborative-doc.test.ts @@ -0,0 +1,61 @@ +// +// 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 { formatCollaborativeDoc } from '@hcengineering/core' +import { collaborativeHistoryDocId, isEditableDoc, isEditableDocVersion } from '../collaborative-doc' + +describe('collaborative-doc', () => { + describe('collaborativeHistoryDocId', () => { + it('returns valid history doc id', async () => { + expect(collaborativeHistoryDocId('minioDocumentId')).toEqual('minioDocumentId#history') + }) + + it('returns valid history doc id for history doc id', async () => { + expect(collaborativeHistoryDocId('minioDocumentId#history')).toEqual('minioDocumentId#history') + }) + }) + + describe('isEditableDoc', () => { + it('returns true for HEAD version', async () => { + const doc = formatCollaborativeDoc({ + documentId: 'example', + versionId: 'HEAD', + revisionId: '0' + }) + expect(isEditableDoc(doc)).toBeTruthy() + }) + + it('returns false for other versions', async () => { + const doc = formatCollaborativeDoc({ + documentId: 'example', + versionId: 'main', + revisionId: '0' + }) + expect(isEditableDoc(doc)).toBeFalsy() + }) + }) + + describe('isEditableDocVersion', () => { + it('returns true for HEAD version', async () => { + expect(isEditableDocVersion('HEAD')).toBeTruthy() + }) + + it('returns false for other versions', async () => { + expect(isEditableDocVersion('')).toBeFalsy() + expect(isEditableDocVersion('main')).toBeFalsy() + expect(isEditableDocVersion('head')).toBeFalsy() + }) + }) +}) diff --git a/server/collaboration/src/utils/__tests__/ydoc.test.ts b/server/collaboration/src/utils/__tests__/ydoc.test.ts new file mode 100644 index 0000000000..c0956bf110 --- /dev/null +++ b/server/collaboration/src/utils/__tests__/ydoc.test.ts @@ -0,0 +1,66 @@ +// +// 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 { Doc as YDoc, XmlElement as YXmlElement, XmlText as YXmlText, encodeStateVector } from 'yjs' + +import { yDocCopyXmlField, yDocFromBuffer, yDocToBuffer } from '../ydoc' + +describe('ydoc', () => { + it('yDocFromBuffer converts ydoc to a buffer', async () => { + const ydoc = new YDoc() + const buffer = yDocToBuffer(ydoc) + + expect(buffer).toBeDefined() + }) + + it('yDocFromBuffer converts buffer to a ydoc', async () => { + const source = new YDoc() + source.getArray('data').insert(0, [1, 2]) + + const buffer = yDocToBuffer(source) + + const target = yDocFromBuffer(buffer, new YDoc()) + expect(target).toBeDefined() + expect(encodeStateVector(target)).toEqual(encodeStateVector(source)) + }) + + describe('yDocCopyXmlField', () => { + it('copies into new field', async () => { + const ydoc = new YDoc() + + const source = ydoc.getXmlFragment('source') + source.insertAfter(null, [new YXmlElement('p'), new YXmlText('foo'), new YXmlElement('p')]) + + yDocCopyXmlField(ydoc, 'source', 'target') + const target = ydoc.getXmlFragment('target') + + expect(target.toJSON()).toEqual(source.toJSON()) + }) + + it('copies into existing field', async () => { + const ydoc = new YDoc() + + const source = ydoc.getXmlFragment('source') + const target = ydoc.getXmlFragment('target') + + source.insertAfter(null, [new YXmlElement('p'), new YXmlText('foo'), new YXmlElement('p')]) + target.insertAfter(null, [new YXmlText('bar')]) + expect(target.toJSON()).not.toEqual(source.toJSON()) + + yDocCopyXmlField(ydoc, 'source', 'target') + expect(target.toJSON()).toEqual(source.toJSON()) + }) + }) +}) diff --git a/server/collaboration/src/utils/collaborative-doc.ts b/server/collaboration/src/utils/collaborative-doc.ts new file mode 100644 index 0000000000..a928ed8b0c --- /dev/null +++ b/server/collaboration/src/utils/collaborative-doc.ts @@ -0,0 +1,197 @@ +// +// 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 { + CollaborativeDoc, + CollaborativeDocVersion, + CollaborativeDocVersionHead, + MeasureContext, + WorkspaceId, + formatCollaborativeDoc, + generateId, + parseCollaborativeDoc +} from '@hcengineering/core' +import { MinioService } from '@hcengineering/minio' +import { Doc as YDoc } from 'yjs' + +import { yDocBranch } from '../history/branch' +import { restoreYdocSnapshot } from '../history/snapshot' +import { yDocFromMinio, yDocToMinio } from './minio' + +/** @public */ +export function collaborativeHistoryDocId (id: string): string { + const suffix = '#history' + return id.endsWith(suffix) ? id : id + suffix +} + +/** @public */ +export async function loadCollaborativeDoc ( + minio: MinioService, + workspace: WorkspaceId, + collaborativeDoc: CollaborativeDoc, + ctx: MeasureContext +): Promise { + const { documentId, versionId } = parseCollaborativeDoc(collaborativeDoc) + return await loadCollaborativeDocVersion(minio, workspace, documentId, versionId, ctx) +} + +/** @public */ +export async function loadCollaborativeDocVersion ( + minio: MinioService, + workspace: WorkspaceId, + documentId: string, + versionId: CollaborativeDocVersion, + ctx: MeasureContext +): Promise { + const historyDocumentId = collaborativeHistoryDocId(documentId) + + return await ctx.with('loadCollaborativeDoc', { type: 'content' }, async (ctx) => { + const yContent = await ctx.with('yDocFromMinio', { type: 'content' }, async () => { + return await yDocFromMinio(minio, workspace, documentId, new YDoc({ gc: false })) + }) + + if (versionId === 'HEAD') { + return yContent + } else { + const yHistory = await ctx.with('yDocFromMinio', { type: 'history' }, async () => { + return await yDocFromMinio(minio, workspace, historyDocumentId, new YDoc({ gc: false })) + }) + + return await ctx.with('restoreYdocSnapshot', {}, () => { + return restoreYdocSnapshot(yContent, yHistory, versionId) + }) + } + }) +} + +/** @public */ +export async function saveCollaborativeDoc ( + minio: MinioService, + workspace: WorkspaceId, + collaborativeDoc: CollaborativeDoc, + ydoc: YDoc, + ctx: MeasureContext +): Promise { + const { documentId, versionId } = parseCollaborativeDoc(collaborativeDoc) + await saveCollaborativeDocVersion(minio, workspace, documentId, versionId, ydoc, ctx) +} + +/** @public */ +export async function saveCollaborativeDocVersion ( + minio: MinioService, + workspace: WorkspaceId, + documentId: string, + versionId: CollaborativeDocVersion, + ydoc: YDoc, + ctx: MeasureContext +): Promise { + await ctx.with('saveCollaborativeDoc', {}, async (ctx) => { + if (versionId === 'HEAD') { + await ctx.with('yDocToMinio', {}, async () => { + await yDocToMinio(minio, workspace, documentId, ydoc) + }) + } else { + console.warn('Cannot save non HEAD document version', documentId, versionId) + } + }) +} + +/** @public */ +export async function removeCollaborativeDoc ( + minio: MinioService, + workspace: WorkspaceId, + collaborativeDocs: CollaborativeDoc[], + ctx: MeasureContext +): Promise { + await ctx.with('removeollaborativeDoc', {}, async (ctx) => { + const toRemove: string[] = [] + for (const collaborativeDoc of collaborativeDocs) { + const { documentId, versionId } = parseCollaborativeDoc(collaborativeDoc) + if (versionId === CollaborativeDocVersionHead) { + toRemove.push(documentId, collaborativeHistoryDocId(documentId)) + } else { + console.warn('Cannot remove non HEAD document version', documentId, versionId) + } + } + if (toRemove.length > 0) { + await ctx.with('remove', {}, async () => { + await minio.remove(workspace, toRemove) + }) + } + }) +} + +/** @public */ +export async function copyCollaborativeDoc ( + minio: MinioService, + workspace: WorkspaceId, + source: CollaborativeDoc, + target: CollaborativeDoc, + ctx: MeasureContext +): Promise { + const { documentId: sourceDocumentId, versionId: sourceVersionId } = parseCollaborativeDoc(source) + const { documentId: targetDocumentId, versionId: targetVersionId } = parseCollaborativeDoc(target) + + if (sourceDocumentId === targetDocumentId) { + // no need to copy into itself + return + } + + await ctx.with('copyCollaborativeDoc', {}, async (ctx) => { + const ySource = await ctx.with('loadCollaborativeDocVersion', {}, async (ctx) => { + return await loadCollaborativeDocVersion(minio, workspace, sourceDocumentId, sourceVersionId, ctx) + }) + + if (ySource === undefined) { + return + } + + const yTarget = await ctx.with('yDocBranch', {}, () => { + return yDocBranch(ySource) + }) + + await ctx.with('saveCollaborativeDocVersion', {}, async (ctx) => { + await saveCollaborativeDocVersion(minio, workspace, targetDocumentId, targetVersionId, yTarget, ctx) + }) + }) +} + +/** @public */ +export function touchCollaborativeDoc (collaborativeDoc: CollaborativeDoc, revisionId?: string): CollaborativeDoc { + revisionId ??= generateId() + const { documentId, versionId } = parseCollaborativeDoc(collaborativeDoc) + return formatCollaborativeDoc({ documentId, versionId, revisionId }) +} + +/** @public */ +export function isEditableDoc (id: CollaborativeDoc): boolean { + const data = parseCollaborativeDoc(id) + return isEditableDocVersion(data.versionId) +} + +/** @public */ +export function isReadonlyDoc (id: CollaborativeDoc): boolean { + return !isEditableDoc(id) +} + +/** @public */ +export function isEditableDocVersion (version: CollaborativeDocVersion): boolean { + return version === CollaborativeDocVersionHead +} + +/** @public */ +export function isReadonlyDocVersion (version: CollaborativeDocVersion): boolean { + return !isEditableDocVersion(version) +} diff --git a/server/collaboration/src/utils/minio.ts b/server/collaboration/src/utils/minio.ts new file mode 100644 index 0000000000..b838fc15ad --- /dev/null +++ b/server/collaboration/src/utils/minio.ts @@ -0,0 +1,51 @@ +// +// 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 { WorkspaceId } from '@hcengineering/core' +import { MinioService } from '@hcengineering/minio' +import { Doc as YDoc } from 'yjs' + +import { yDocFromBuffer, yDocToBuffer } from './ydoc' + +/** @public */ +export async function yDocFromMinio ( + minio: MinioService, + workspace: WorkspaceId, + minioDocumentId: string, + ydoc?: YDoc +): Promise { + // no need to apply gc because we load existing document + // it is either already gc-ed, or gc not needed and it is disabled + ydoc ??= new YDoc({ gc: false }) + + try { + const buffer = await minio.read(workspace, minioDocumentId) + return yDocFromBuffer(Buffer.concat(buffer), ydoc) + } catch (err) { + throw new Error('Failed to load ydoc from minio', { cause: err }) + } +} + +/** @public */ +export async function yDocToMinio ( + minio: MinioService, + workspace: WorkspaceId, + minioDocumentId: string, + ydoc: YDoc +): Promise { + const buffer = yDocToBuffer(ydoc) + const metadata = { 'content-type': 'application/ydoc' } + await minio.put(workspace, minioDocumentId, buffer, buffer.length, metadata) +} diff --git a/server/collaboration/src/utils/ydoc.ts b/server/collaboration/src/utils/ydoc.ts new file mode 100644 index 0000000000..ecb1e5d843 --- /dev/null +++ b/server/collaboration/src/utils/ydoc.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 { AbstractType as YAbstractType, Doc as YDoc, applyUpdate, encodeStateAsUpdate } from 'yjs' + +/** @public */ +export function yDocFromBuffer (buffer: Buffer, ydoc: YDoc): YDoc { + try { + const uint8arr = new Uint8Array(buffer) + applyUpdate(ydoc, uint8arr) + return ydoc + } catch (err) { + throw new Error('Failed to apply ydoc update', { cause: err }) + } +} + +/** @public */ +export function yDocToBuffer (ydoc: YDoc): Buffer { + const update = encodeStateAsUpdate(ydoc) + return Buffer.from(update.buffer) +} + +/** @public */ +export function yDocCopyXmlField (ydoc: YDoc, source: string, target: string): void { + const srcField = ydoc.getXmlFragment(source) + const dstField = ydoc.getXmlFragment(target) + + ydoc.transact((tr) => { + // similar to XmlFragment's clone method + dstField.delete(0, dstField.length) + dstField.insert(0, srcField.toArray().map((item) => (item instanceof YAbstractType ? item.clone() : item)) as any) + }) +} diff --git a/server/collaboration/tsconfig.json b/server/collaboration/tsconfig.json new file mode 100644 index 0000000000..4e0fa3fe7f --- /dev/null +++ b/server/collaboration/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/server/collaborator/package.json b/server/collaborator/package.json index 7f2eafeee2..b642980306 100644 --- a/server/collaborator/package.json +++ b/server/collaborator/package.json @@ -53,10 +53,11 @@ "@hcengineering/server-tool": "^0.6.0", "@hcengineering/server-token": "^0.6.7", "@hcengineering/server-core": "^0.6.1", - "@hcengineering/attachment": "^0.6.9", "@hcengineering/client": "^0.6.14", "@hcengineering/client-resources": "^0.6.23", "@hcengineering/minio": "^0.6.0", + "@hcengineering/collaboration": "^0.6.0", + "@hcengineering/collaborator-client": "^0.6.0", "@hcengineering/text": "^0.6.1", "@hocuspocus/server": "^2.9.0", "@hocuspocus/transformer": "^2.9.0", diff --git a/server/collaborator/src/rpc/index.ts b/server/collaborator/src/rpc/index.ts new file mode 100644 index 0000000000..d034bc4896 --- /dev/null +++ b/server/collaborator/src/rpc/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 './methods' +export * from './rpc' diff --git a/server/collaborator/src/rpc/methods/branchDocument.ts b/server/collaborator/src/rpc/methods/branchDocument.ts new file mode 100644 index 0000000000..02fcc4acfd --- /dev/null +++ b/server/collaborator/src/rpc/methods/branchDocument.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 { yDocBranchWithGC } from '@hcengineering/collaboration' +import { type BranchDocumentRequest, type BranchDocumentResponse } from '@hcengineering/collaborator-client' +import { MeasureContext } from '@hcengineering/core' +import { applyUpdate, encodeStateAsUpdate } from 'yjs' +import { Context } from '../../context' +import { RpcMethodParams } from '../rpc' + +export async function branchDocument ( + ctx: MeasureContext, + context: Context, + payload: BranchDocumentRequest, + params: RpcMethodParams +): Promise { + const { sourceDocumentId, targetDocumentId } = payload + const { hocuspocus } = params + + const sourceConnection = await ctx.with('connect', { type: 'source' }, async () => { + return await hocuspocus.openDirectConnection(sourceDocumentId, context) + }) + + const targetConnection = await ctx.with('connect', { type: 'target' }, async () => { + return await hocuspocus.openDirectConnection(targetDocumentId, context) + }) + + try { + let update = new Uint8Array() + + await sourceConnection.transact((document) => { + const copy = yDocBranchWithGC(document) + update = encodeStateAsUpdate(copy) + }) + + await targetConnection.transact((document) => { + applyUpdate(document, update) + }) + } finally { + await sourceConnection.disconnect() + await targetConnection.disconnect() + } + + return {} +} diff --git a/server/collaborator/src/rpc/methods/copyContent.ts b/server/collaborator/src/rpc/methods/copyContent.ts new file mode 100644 index 0000000000..d9c07b8f48 --- /dev/null +++ b/server/collaborator/src/rpc/methods/copyContent.ts @@ -0,0 +1,44 @@ +// +// 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 { yDocCopyXmlField } from '@hcengineering/collaboration' +import { type CopyContentRequest, type CopyContentResponse } from '@hcengineering/collaborator-client' +import { MeasureContext } from '@hcengineering/core' +import { Context } from '../../context' +import { RpcMethodParams } from '../rpc' + +export async function copyContent ( + ctx: MeasureContext, + context: Context, + payload: CopyContentRequest, + params: RpcMethodParams +): Promise { + const { documentId, sourceField, targetField } = payload + const { hocuspocus } = params + + const connection = await ctx.with('connect', {}, async () => { + return await hocuspocus.openDirectConnection(documentId, context) + }) + + try { + await connection.transact((document) => { + yDocCopyXmlField(document, sourceField, targetField) + }) + } finally { + await connection.disconnect() + } + + return {} +} diff --git a/server/collaborator/src/rpc/methods/getContent.ts b/server/collaborator/src/rpc/methods/getContent.ts new file mode 100644 index 0000000000..3e53f2c3ea --- /dev/null +++ b/server/collaborator/src/rpc/methods/getContent.ts @@ -0,0 +1,47 @@ +// +// 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 { MeasureContext } from '@hcengineering/core' +import { type GetContentRequest, type GetContentResponse } from '@hcengineering/collaborator-client' +import { Context } from '../../context' +import { RpcMethodParams } from '../rpc' + +export async function getContent ( + ctx: MeasureContext, + context: Context, + payload: GetContentRequest, + params: RpcMethodParams +): Promise { + const { documentId, field } = payload + const { hocuspocus, transformer } = params + + const connection = await ctx.with('connect', {}, async () => { + return await hocuspocus.openDirectConnection(documentId, context) + }) + + try { + const html = await ctx.with('transform', {}, async () => { + let content = '' + await connection.transact((document) => { + content = transformer.fromYdoc(document, field) + }) + return content + }) + + return { html } + } finally { + await connection.disconnect() + } +} diff --git a/server/collaborator/src/rpc/methods/index.ts b/server/collaborator/src/rpc/methods/index.ts new file mode 100644 index 0000000000..c151e9e085 --- /dev/null +++ b/server/collaborator/src/rpc/methods/index.ts @@ -0,0 +1,31 @@ +// +// 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 { getContent } from './getContent' +import { copyContent } from './copyContent' +import { updateContent } from './updateContent' +import { branchDocument } from './branchDocument' +import { removeDocument } from './removeDocument' +import { takeSnapshot } from './takeSnapshot' +import { RpcMethod } from '../rpc' + +export const methods: Record = { + getContent, + copyContent, + updateContent, + branchDocument, + removeDocument, + takeSnapshot +} diff --git a/server/collaborator/src/rpc/methods/removeDocument.ts b/server/collaborator/src/rpc/methods/removeDocument.ts new file mode 100644 index 0000000000..3052984a49 --- /dev/null +++ b/server/collaborator/src/rpc/methods/removeDocument.ts @@ -0,0 +1,48 @@ +// +// 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 { collaborativeHistoryDocId } from '@hcengineering/collaboration' +import { type RemoveDocumentRequest, type RemoveDocumentResponse } from '@hcengineering/collaborator-client' +import { MeasureContext, parseCollaborativeDoc } from '@hcengineering/core' +import { Context } from '../../context' +import { RpcMethodParams } from '../rpc' + +export async function removeDocument ( + ctx: MeasureContext, + context: Context, + payload: RemoveDocumentRequest, + params: RpcMethodParams +): Promise { + const { documentId, collaborativeDoc } = payload + const { hocuspocus, minio } = params + const { workspaceId } = context + + const document = hocuspocus.documents.get(documentId) + if (document !== undefined) { + hocuspocus.closeConnections(documentId) + hocuspocus.unloadDocument(document) + } + + const { documentId: minioDocumentId } = parseCollaborativeDoc(collaborativeDoc) + const historyDocumentId = collaborativeHistoryDocId(minioDocumentId) + + try { + await minio.remove(workspaceId, [minioDocumentId, historyDocumentId]) + } catch (err) { + console.error(err) + } + + return {} +} diff --git a/server/collaborator/src/rpc/methods/takeSnapshot.ts b/server/collaborator/src/rpc/methods/takeSnapshot.ts new file mode 100644 index 0000000000..7980e79f91 --- /dev/null +++ b/server/collaborator/src/rpc/methods/takeSnapshot.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 { + YDocVersion, + collaborativeHistoryDocId, + createYdocSnapshot, + yDocFromMinio, + yDocToMinio +} from '@hcengineering/collaboration' +import { type TakeSnapshotRequest, type TakeSnapshotResponse } from '@hcengineering/collaborator-client' +import { CollaborativeDocVersionHead, MeasureContext, generateId, parseCollaborativeDoc } from '@hcengineering/core' +import { Doc as YDoc } from 'yjs' +import { Context } from '../../context' +import { RpcMethodParams } from '../rpc' + +export async function takeSnapshot ( + ctx: MeasureContext, + context: Context, + payload: TakeSnapshotRequest, + params: RpcMethodParams +): Promise { + const { collaborativeDoc, documentId, snapshotName, createdBy } = payload + const { hocuspocus, minio } = params + const { workspaceId } = context + + const version: YDocVersion = { + versionId: generateId(), + name: snapshotName, + createdBy, + createdOn: Date.now() + } + + const { documentId: minioDocumentId, versionId } = parseCollaborativeDoc(collaborativeDoc) + if (versionId !== CollaborativeDocVersionHead) { + throw new Error('invalid document version') + } + + const connection = await ctx.with('connect', {}, async () => { + return await hocuspocus.openDirectConnection(documentId, context) + }) + + try { + // load history document directly from minio + const historyDocumentId = collaborativeHistoryDocId(minioDocumentId) + const yHistory = await ctx.with('yDocFromMinio', {}, async () => { + try { + return await yDocFromMinio(minio, workspaceId, historyDocumentId) + } catch { + return new YDoc() + } + }) + + await ctx.with('createYdocSnapshot', {}, async () => { + await connection.transact((yContent) => { + createYdocSnapshot(yContent, yHistory, version) + }) + }) + + await ctx.with('yDocToMinio', {}, async () => { + await yDocToMinio(minio, workspaceId, historyDocumentId, yHistory) + }) + + return { ...version } + } finally { + await connection.disconnect() + } +} diff --git a/server/collaborator/src/rpc/methods/updateContent.ts b/server/collaborator/src/rpc/methods/updateContent.ts new file mode 100644 index 0000000000..f7ae19d63a --- /dev/null +++ b/server/collaborator/src/rpc/methods/updateContent.ts @@ -0,0 +1,55 @@ +// +// 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 { MeasureContext } from '@hcengineering/core' +import { type UpdateContentRequest, type UpdateContentResponse } from '@hcengineering/collaborator-client' +import { applyUpdate, encodeStateAsUpdate } from 'yjs' +import { Context } from '../../context' +import { RpcMethodParams } from '../rpc' + +export async function updateContent ( + ctx: MeasureContext, + context: Context, + payload: UpdateContentRequest, + params: RpcMethodParams +): Promise { + const { documentId, field, html } = payload + const { hocuspocus, transformer } = params + + const update = await ctx.with('transform', {}, () => { + const ydoc = transformer.toYdoc(html, field) + return encodeStateAsUpdate(ydoc) + }) + + const connection = await ctx.with('connect', {}, async () => { + return await hocuspocus.openDirectConnection(documentId, context) + }) + + try { + await ctx.with('update', {}, async () => { + await connection.transact((document) => { + const fragment = document.getXmlFragment(field) + document.transact(() => { + fragment.delete(0, fragment.length) + applyUpdate(document, update) + }) + }) + }) + } finally { + await connection.disconnect() + } + + return {} +} diff --git a/server/collaborator/src/rpc/rpc.ts b/server/collaborator/src/rpc/rpc.ts new file mode 100644 index 0000000000..e233a9f422 --- /dev/null +++ b/server/collaborator/src/rpc/rpc.ts @@ -0,0 +1,44 @@ +// +// 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 { MeasureContext } from '@hcengineering/core' +import { MinioService } from '@hcengineering/minio' +import { Hocuspocus } from '@hocuspocus/server' +import { Transformer } from '@hocuspocus/transformer' +import { Context } from '../context' + +export interface RpcRequest { + method: string + payload: object +} + +export interface RpcErrorResponse { + error: string +} + +export type RpcResponse = object | RpcErrorResponse + +export type RpcMethod = ( + ctx: MeasureContext, + context: Context, + payload: any, + params: RpcMethodParams +) => Promise + +export interface RpcMethodParams { + hocuspocus: Hocuspocus + minio: MinioService + transformer: Transformer +} diff --git a/server/collaborator/src/server.ts b/server/collaborator/src/server.ts index 1b29be4927..3585a9a941 100644 --- a/server/collaborator/src/server.ts +++ b/server/collaborator/src/server.ts @@ -13,6 +13,7 @@ // limitations under the License. // +import { isReadonlyDocVersion } from '@hcengineering/collaboration' import { MeasureContext, generateId } from '@hcengineering/core' import { MinioService } from '@hcengineering/minio' import { Token, decodeToken } from '@hcengineering/server-token' @@ -25,7 +26,6 @@ import express from 'express' import { IncomingMessage, createServer } from 'http' import { MongoClient } from 'mongodb' import { WebSocket, WebSocketServer } from 'ws' -import { applyUpdate, encodeStateAsUpdate } from 'yjs' import { getWorkspaceInfo } from './account' import { Config } from './config' @@ -34,12 +34,11 @@ import { ActionsExtension } from './extensions/action' import { HtmlTransformer } from './transformers/html' import { StorageExtension } from './extensions/storage' import { Controller, getClientFactory } from './platform' -import { MinioStorageAdapter } from './storage/minio' +import { MinioStorageAdapter, parseDocumentId } from './storage/minio' import { MongodbStorageAdapter } from './storage/mongodb' import { PlatformStorageAdapter } from './storage/platform' import { RouterStorageAdapter } from './storage/router' - -const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0' +import { RpcErrorResponse, RpcRequest, RpcResponse, methods } from './rpc' /** * @public @@ -112,8 +111,10 @@ export async function start ( * options to pass to the ydoc document */ yDocOptions: { - gc: gcEnabled, - gcFilter: () => true + // we intentionally disable gc in order to make snapshots working + // see https://github.com/yjs/yjs/blob/v13.5.52/src/utils/Snapshot.js#L162 + gc: false, + gcFilter: () => false }, /** * If set to false, respects the debounce time of `onStoreDocument` before unloading a document. @@ -144,29 +145,24 @@ export async function start ( async onAuthenticate (data: onAuthenticatePayload): Promise { ctx.measure('authenticate', 1) - const context = buildContext(data, controller) - // verify workspace can be accessed with the token - const workspaceInfo = await getWorkspaceInfo(data.token) - - // verify document name let documentName = data.documentName if (documentName.includes('://')) { documentName = documentName.split('://', 2)[1] } - if (documentName.includes('/')) { - const [workspaceUrl] = documentName.split('/', 2) + const { workspaceUrl, versionId } = parseDocumentId(documentName) - // verify workspace url in the document matches the token - if (workspaceInfo.workspace !== workspaceUrl) { - throw new Error('documentName must include workspace') - } - } else { + // verify workspace can be accessed with the token + const workspaceInfo = await getWorkspaceInfo(data.token) + // verify workspace url in the document matches the token + if (workspaceInfo.workspace !== workspaceUrl) { throw new Error('documentName must include workspace') } - return context + data.connection.readOnly = isReadonlyDocVersion(versionId) + + return buildContext(data, controller) }, async onDestroy (data: onDestroyPayload): Promise { @@ -174,7 +170,7 @@ export async function start ( } }) - const restCtx = ctx.newChild('REST', {}) + const rpcCtx = ctx.newChild('rpc', {}) const getContext = (token: Token, initialContentId?: string): Context => { return { @@ -187,117 +183,33 @@ export async function start ( } // eslint-disable-next-line @typescript-eslint/no-misused-promises - app.get('/api/content/:documentId/:field', async (req, res) => { - console.log('handle request', req.method, req.url) - + app.post('/rpc', async (req, res) => { const authHeader = req.headers.authorization if (authHeader === undefined) { - res.status(403).send() + res.status(403).send({ error: 'Unauthorized' }) return } - const token = authHeader.split(' ')[1] - const decodedToken = decodeToken(token) + const token = decodeToken(authHeader.split(' ')[1]) + const context = getContext(token) - const documentId = req.params.documentId - const field = req.params.field - const initialContentId = req.query.initialContentId as string - - if (documentId === undefined || documentId === '') { - res.status(400).send({ err: "'documentId' is missing" }) - return - } - - if (field === undefined || field === '') { - res.status(400).send({ err: "'field' is missing" }) - return - } - - const context = getContext(decodedToken, initialContentId) - - await restCtx.with(`${req.method} /content`, {}, async (ctx) => { - const connection = await ctx.with('connect', {}, async () => { - return await hocuspocus.openDirectConnection(documentId, context) - }) - - try { - const html = await ctx.with('transform', {}, async () => { - let content = '' - await connection.transact((document) => { - content = transformer.fromYdoc(document, field) - }) - return content - }) - - res.writeHead(200, { 'Content-Type': 'application/json' }) - const json = JSON.stringify({ html }) - res.end(json) - } catch (err: any) { - res.status(500).send({ message: err.message }) - } finally { - await connection.disconnect() + const request = req.body as RpcRequest + const method = methods[request.method] + if (method === undefined) { + const response: RpcErrorResponse = { + error: 'Unknown method' } - }) - - res.end() - }) - - // eslint-disable-next-line @typescript-eslint/no-misused-promises - app.put('/api/content/:documentId/:field', async (req, res) => { - console.log('handle request', req.method, req.url) - - const authHeader = req.headers.authorization - if (authHeader === undefined) { - res.status(403).send() - return - } - - const token = authHeader.split(' ')[1] - const decodedToken = decodeToken(token) - - const documentId = req.params.documentId - const field = req.params.field - const initialContentId = req.query.initialContentId as string - const data = req.body.html ?? '

' - - if (documentId === undefined || documentId === '') { - res.status(400).send({ err: "'documentId' is missing" }) - return - } - - if (field === undefined || field === '') { - res.status(400).send({ err: "'field' is missing" }) - return - } - - const context = getContext(decodedToken, initialContentId) - - await restCtx.with(`${req.method} /content`, {}, async (ctx) => { - const update = await ctx.with('transform', {}, () => { - const ydoc = transformer.toYdoc(data, field) - return encodeStateAsUpdate(ydoc) + res.status(400).send(response) + } else { + await rpcCtx.with(request.method, {}, async (ctx) => { + try { + const response: RpcResponse = await method(ctx, context, request.payload, { hocuspocus, minio, transformer }) + res.status(200).send(response) + } catch (err: any) { + res.status(500).send({ error: err.message }) + } }) - - const connection = await ctx.with('connect', {}, async () => { - return await hocuspocus.openDirectConnection(documentId, context) - }) - - try { - await ctx.with('update', {}, async () => { - await connection.transact((document) => { - const fragment = document.getXmlFragment(field) - document.transact((tr) => { - fragment.delete(0, fragment.length) - applyUpdate(document, update) - }) - }) - }) - } finally { - await connection.disconnect() - } - }) - - res.status(200).end() + } }) const wss = new WebSocketServer({ diff --git a/server/collaborator/src/storage/minio.ts b/server/collaborator/src/storage/minio.ts index 1e76f0a8c2..c764216496 100644 --- a/server/collaborator/src/storage/minio.ts +++ b/server/collaborator/src/storage/minio.ts @@ -1,5 +1,5 @@ // -// Copyright © 2023 Hardcore Engineering Inc. +// Copyright © 2023, 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 @@ -13,34 +13,32 @@ // limitations under the License. // -import attachment, { Attachment } from '@hcengineering/attachment' -import { MeasureContext, Ref } from '@hcengineering/core' +import { loadCollaborativeDocVersion, saveCollaborativeDocVersion } from '@hcengineering/collaboration' +import { CollaborativeDocVersion, CollaborativeDocVersionHead, MeasureContext } from '@hcengineering/core' import { MinioService } from '@hcengineering/minio' -import { Doc as YDoc, applyUpdate, encodeStateAsUpdate } from 'yjs' +import { Doc as YDoc } from 'yjs' import { Context } from '../context' import { StorageAdapter } from './adapter' -interface MinioDocumentId { +export interface MinioDocumentId { workspaceUrl: string minioDocumentId: string + versionId: CollaborativeDocVersion } -function parseDocumentId (documentId: string): MinioDocumentId { - const [workspaceUrl, minioDocumentId] = documentId.split('/') +export function parseDocumentId (documentId: string): MinioDocumentId { + const [workspaceUrl, minioDocumentId, versionId] = documentId.split('/') return { workspaceUrl: workspaceUrl ?? '', - minioDocumentId: minioDocumentId ?? '' + minioDocumentId: minioDocumentId ?? '', + versionId: versionId ?? CollaborativeDocVersionHead } } function isValidDocumentId (documentId: MinioDocumentId): boolean { - return documentId.minioDocumentId !== '' && documentId.workspaceUrl !== '' -} - -function maybePlatformDocumentId (documentId: string): boolean { - return !documentId.includes('%') + return documentId.workspaceUrl !== '' && documentId.minioDocumentId !== '' && documentId.versionId !== '' } export class MinioStorageAdapter implements StorageAdapter { @@ -52,86 +50,34 @@ export class MinioStorageAdapter implements StorageAdapter { async loadDocument (documentId: string, context: Context): Promise { const { workspaceId } = context - const { workspaceUrl, minioDocumentId } = parseDocumentId(documentId) + const { workspaceUrl, minioDocumentId, versionId } = parseDocumentId(documentId) - if (!isValidDocumentId({ workspaceUrl, minioDocumentId })) { + if (!isValidDocumentId({ workspaceUrl, minioDocumentId, versionId })) { console.warn('malformed document id', documentId) return undefined } return await this.ctx.with('load-document', {}, async (ctx) => { - const minioDocument = await ctx.with('query', {}, async () => { - try { - const buffer = await this.minio.read(workspaceId, minioDocumentId) - return Buffer.concat(buffer) - } catch { - return undefined - } - }) - - if (minioDocument === undefined) { + try { + return await loadCollaborativeDocVersion(this.minio, workspaceId, minioDocumentId, versionId, ctx) + } catch { return undefined } - - const ydoc = new YDoc() - - await ctx.with('transform', {}, () => { - try { - const uint8arr = new Uint8Array(minioDocument) - applyUpdate(ydoc, uint8arr) - } catch (err) { - console.error(err) - } - }) - - return ydoc }) } async saveDocument (documentId: string, document: YDoc, context: Context): Promise { - const { clientFactory, workspaceId } = context + const { workspaceId } = context - const { workspaceUrl, minioDocumentId } = parseDocumentId(documentId) + const { workspaceUrl, minioDocumentId, versionId } = parseDocumentId(documentId) - if (!isValidDocumentId({ workspaceUrl, minioDocumentId })) { + if (!isValidDocumentId({ workspaceUrl, minioDocumentId, versionId })) { console.warn('malformed document id', documentId) return undefined } await this.ctx.with('save-document', {}, async (ctx) => { - const buffer = await ctx.with('transform', {}, () => { - const updates = encodeStateAsUpdate(document) - return Buffer.from(updates.buffer) - }) - - await ctx.with('update', {}, async () => { - const metadata = { 'content-type': 'application/ydoc' } - await this.minio.put(workspaceId, minioDocumentId, buffer, buffer.length, metadata) - }) - - // minio file is usually an attachment document - // we need to touch an attachment from here to notify platform about changes - - if (!maybePlatformDocumentId(minioDocumentId)) { - // documentId is not a platform document id, we can skip platform notification - return - } - - await ctx.with('platform', {}, async () => { - const client = await ctx.with('connect', {}, async () => { - return await clientFactory({ derived: true }) - }) - - const current = await ctx.with('query', {}, async () => { - return await client.findOne(attachment.class.Attachment, { _id: minioDocumentId as Ref }) - }) - - if (current !== undefined) { - await ctx.with('update', {}, async () => { - await client.update(current, { lastModified: Date.now(), size: buffer.length }) - }) - } - }) + await saveCollaborativeDocVersion(this.minio, workspaceId, minioDocumentId, versionId, document, ctx) }) } } diff --git a/server/collaborator/src/storage/platform.ts b/server/collaborator/src/storage/platform.ts index 11961cb63b..6579d349a7 100644 --- a/server/collaborator/src/storage/platform.ts +++ b/server/collaborator/src/storage/platform.ts @@ -13,7 +13,8 @@ // limitations under the License. // -import { Class, Doc, MeasureContext, Ref } from '@hcengineering/core' +import { touchCollaborativeDoc } from '@hcengineering/collaboration' +import core, { Class, CollaborativeDoc, Doc, MeasureContext, Ref } from '@hcengineering/core' import { Transformer } from '@hocuspocus/transformer' import { Doc as YDoc } from 'yjs' @@ -52,6 +53,8 @@ export class PlatformStorageAdapter implements StorageAdapter { ) {} async loadDocument (documentId: string, context: Context): Promise { + console.warn('loading documents from the platform not supported', documentId) + const { clientFactory } = context const { workspaceUrl, objectId, objectClass, objectAttr } = parseDocumentId(documentId) @@ -67,6 +70,18 @@ export class PlatformStorageAdapter implements StorageAdapter { return await clientFactory({ derived: false }) }) + const hierarchy = client.getHierarchy() + const attribute = hierarchy.findAttribute(objectClass, objectAttr) + if (attribute === undefined) { + console.warn('invalid attribute', objectAttr) + return undefined + } + + if (hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeMarkup)) { + console.warn('unsupported attribute type', attribute?.type._class) + return undefined + } + const doc = await ctx.with('query', {}, async () => { return await client.findOne(objectClass, { _id: objectId }, { projection: { [objectAttr]: 1 } }) }) @@ -94,18 +109,35 @@ export class PlatformStorageAdapter implements StorageAdapter { return await clientFactory({ derived: false }) }) + const attribute = client.getHierarchy().findAttribute(objectClass, objectAttr) + if (attribute === undefined) { + console.warn('attribute not found', objectClass, objectAttr) + return + } + const current = await ctx.with('query', {}, async () => { return await client.findOne(objectClass, { _id: objectId }) }) - if (current !== undefined) { + if (current === undefined) { + return + } + + const hierarchy = client.getHierarchy() + if (hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeDoc)) { + const collaborativeDoc = (current as any)[objectAttr] as CollaborativeDoc + const newCollaborativeDoc = touchCollaborativeDoc(collaborativeDoc) + + await ctx.with('update', {}, async () => { + await client.diffUpdate(current, { [objectAttr]: newCollaborativeDoc }) + }) + } else if (hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeMarkup)) { + // TODO a temporary solution while we are keeping Markup in Mongo const content = await ctx.with('transform', {}, () => { return this.transformer.fromYdoc(document, objectAttr) }) await ctx.with('update', {}, async () => { - if ((current as any)[objectAttr] !== content) { - await client.update(current, { [objectAttr]: content }) - } + await client.diffUpdate(current, { [objectAttr]: content }) }) } }) diff --git a/server/collaborator/src/types.ts b/server/collaborator/src/types.ts index 7c1ba35ee2..5eb10bd447 100644 --- a/server/collaborator/src/types.ts +++ b/server/collaborator/src/types.ts @@ -13,10 +13,17 @@ // limitations under the License. // +/** @public */ +export interface DocumentId { + workspaceUrl: string + documentId: string + versionId: string +} + +/** @public */ export type Action = DocumentCopyAction | DocumentFieldCopyAction | DocumentContentAction -export type StorageType = 'minio' | 'platform' - +/** @public */ export interface DocumentContentAction { action: 'document.content' params: { @@ -25,6 +32,7 @@ export interface DocumentContentAction { } } +/** @public */ export interface DocumentCopyAction { action: 'document.copy' params: { @@ -33,6 +41,7 @@ export interface DocumentCopyAction { } } +/** @public */ export interface DocumentFieldCopyAction { action: 'document.field.copy' params: { @@ -42,8 +51,10 @@ export interface DocumentFieldCopyAction { } } +/** @public */ export type ActionStatus = 'completed' | 'failed' +/** @public */ export interface ActionStatusResponse { action: Action status: ActionStatus diff --git a/server/front/src/index.ts b/server/front/src/index.ts index 10468a2704..e6caeb3225 100644 --- a/server/front/src/index.ts +++ b/server/front/src/index.ts @@ -148,6 +148,7 @@ export function start ( gmailUrl: string calendarUrl: string collaboratorUrl: string + collaboratorApiUrl: string title?: string languages: string defaultLanguage: string @@ -192,6 +193,7 @@ export function start ( GMAIL_URL: config.gmailUrl, CALENDAR_URL: config.calendarUrl, COLLABORATOR_URL: config.collaboratorUrl, + COLLABORATOR_API_URL: config.collaboratorApiUrl, TITLE: config.title, LANGUAGES: config.languages, DEFAULT_LANGUAGE: config.defaultLanguage, diff --git a/server/front/src/starter.ts b/server/front/src/starter.ts index d40291deef..9cb450d80e 100644 --- a/server/front/src/starter.ts +++ b/server/front/src/starter.ts @@ -104,6 +104,12 @@ export function startFront (extraConfig?: Record): void { process.exit(1) } + const collaboratorApiUrl = process.env.COLLABORATOR_API_URL + if (collaboratorApiUrl === undefined) { + console.error('please provide collaborator api url') + process.exit(1) + } + const modelVersion = process.env.MODEL_VERSION if (modelVersion === undefined) { console.error('please provide model version requirement') @@ -135,6 +141,7 @@ export function startFront (extraConfig?: Record): void { rekoniUrl, calendarUrl, collaboratorUrl, + collaboratorApiUrl, title, languages, defaultLanguage diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml index 534d983dbb..42b9c4ff38 100644 --- a/tests/docker-compose.yaml +++ b/tests/docker-compose.yaml @@ -73,6 +73,7 @@ services: - REKONI_URL=http://rekoni:4005 - TELEGRAM_URL=http://localhost:8086 - COLLABORATOR_URL=ws://localhost:3079 + - COLLABORATOR_API_URL=http://localhost:3079 - MINIO_ENDPOINT=minio - MINIO_ACCESS_KEY=minioadmin - MINIO_SECRET_KEY=minioadmin