UBERF-8425: Global accounts (#7573)

This commit is contained in:
Alexey Zinoviev
2025-02-04 10:31:03 +07:00
committed by GitHub
parent 83e5b16392
commit 2c64ceadb8
765 changed files with 19134 additions and 17955 deletions
+1 -1
View File
@@ -192,7 +192,7 @@ jobs:
- name: Testing...
run: node common/scripts/install-run-rush.js test
env:
DB_URL: 'postgresql://postgres:example@localhost:5433'
DB_URL: 'postgresql://root@localhost:26258/defaultdb?sslmode=disable'
ELASTIC_URL: 'http://localhost:9201'
MONGO_URL: 'mongodb://localhost:27018'
uitest:
+4 -2
View File
@@ -107,6 +107,7 @@
"MONGO_URL": "mongodb://localhost:27017",
"DB_URL": "mongodb://localhost:27017",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
// "DB_URL": "postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable",
"SERVER_SECRET": "secret",
"REGION_INFO":"|Mongo;pg|Postgres;cockroach|CockroachDB",
"TRANSACTOR_URL": "ws://host.docker.internal:3333,ws://host.docker.internal:3331;;pg,ws://host.docker.internal:3332;;cockroach",
@@ -326,6 +327,7 @@
"MONGO_URL": "mongodb://localhost:27017",
"DB_URL": "mongodb://localhost:27017",
"ACCOUNTS_URL": "http://localhost:3000",
"ACCOUNT_DB_URL": "mongodb://localhost:27017",
"TELEGRAM_DATABASE": "telegram-service",
"REKONI_URL": "http://localhost:4004",
"MODEL_VERSION": "0.6.421"
@@ -562,7 +564,7 @@
"MINIO_SECRET_KEY": "minioadmin",
"SERVER_SECRET": "secret",
"SERVICE_ID": "analytics-collector",
"SUPPORT_WORKSPACE": "support",
"SUPPORT_WORKSPACE": "09b65664-b3c3-4ea8-b9a1-c9688bde17f0",
"ACCOUNTS_URL": "http://localhost:3000"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
@@ -582,7 +584,7 @@
"MONGO_URL": "mongodb://localhost:27017",
"PORT": "4010",
"SERVER_SECRET": "secret",
"SUPPORT_WORKSPACE": "support",
"SUPPORT_WORKSPACE": "09b65664-b3c3-4ea8-b9a1-c9688bde17f0",
"FIRST_NAME": "Jolie",
"LAST_NAME": "AI",
"PASSWORD": "password",
+133 -37
View File
@@ -70,6 +70,9 @@ importers:
'@rush-temp/account':
specifier: file:./projects/account.tgz
version: file:projects/account.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
'@rush-temp/account-client':
specifier: file:./projects/account-client.tgz
version: file:projects/account-client.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
'@rush-temp/account-service':
specifier: file:./projects/account-service.tgz
version: file:projects/account-service.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)
@@ -1387,6 +1390,9 @@ importers:
'@types/pdfjs-dist':
specifier: 2.10.378
version: 2.10.378
'@types/pg':
specifier: ^8.11.6
version: 8.11.11
'@types/png-chunks-extract':
specifier: ^1.0.2
version: 1.0.2
@@ -3800,12 +3806,16 @@ packages:
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@rush-temp/account-client@file:projects/account-client.tgz':
resolution: {integrity: sha512-alEMy85WshDCFug+rB3K6vcFRWDj/NyeutHceDQiMW1G01RHNzI+uCSQUqKfAgq6aR/Dq11i9l7TuUxBM3mG7w==, tarball: file:projects/account-client.tgz}
version: 0.0.0
'@rush-temp/account-service@file:projects/account-service.tgz':
resolution: {integrity: sha512-oAW0UD+ThGbtuXdyaoWJoe93XY7bLO0K2uZneePiHsI3KZM3z9st9fjxSbJE4wmC9iE2wmzNYTN6AHrcFxvRrQ==, tarball: file:projects/account-service.tgz}
resolution: {integrity: sha512-SBWP6HBOnzpJC3MIOlYbttcjYw1HoD9VpbLpUS3zRwEL2slU6c0W/hEBU0/rzh3KkLb6bScKssS5utlSx5RrNw==, tarball: file:projects/account-service.tgz}
version: 0.0.0
'@rush-temp/account@file:projects/account.tgz':
resolution: {integrity: sha512-xWdmeIZiSKL5eyvVHNUEnn/GXGsaNkayjxoDW+ydFB1GwUWExvSTA7OOs9mfG+IwLNU08GNKNGBYGM3XKZ2PsA==, tarball: file:projects/account.tgz}
resolution: {integrity: sha512-NN+0JN9QcVA0sk5OmSpSwhjf0lhiWqXFztNPSOZbNnyziTzKgufXiLjho7Nwubi+aev+EIL8weUdW93QBYh07A==, tarball: file:projects/account.tgz}
version: 0.0.0
'@rush-temp/activity-assets@file:projects/activity-assets.tgz':
@@ -3849,7 +3859,7 @@ packages:
version: 0.0.0
'@rush-temp/api-client@file:projects/api-client.tgz':
resolution: {integrity: sha512-tyhf+5DHkF+HpWZlimhdgls3Gp3L7PR0UHYEu0YH7e0UmOoZSuvAVbRbwY/6UnVG1am+xQnrs0xiZ93AHIsgBA==, tarball: file:projects/api-client.tgz}
resolution: {integrity: sha512-lFsfqbx6XIWKo8wq95O25j0RYJ9pUMgtb3T/tVUKfWG36NUuImICpStaQRmcm6MPYq7Gtb0KKuOXyQJg3y/80w==, tarball: file:projects/api-client.tgz}
version: 0.0.0
'@rush-temp/attachment-assets@file:projects/attachment-assets.tgz':
@@ -3957,7 +3967,7 @@ packages:
version: 0.0.0
'@rush-temp/collaborator@file:projects/collaborator.tgz':
resolution: {integrity: sha512-+HMz/V8Ne3kDYebHozkhIZW49OAaf89689gEqbe8KXMUs0hAJG7Fe4Ns5uQp7PqZBEiKoILlfNDJEJPvBRR4tQ==, tarball: file:projects/collaborator.tgz}
resolution: {integrity: sha512-QNIE09ZcEdRCPC+oX1x4ltXrEJ+GDrz23EhmqdEZiiU3gLpj6RTVjGnGLeyKTjmXhPWorKWAGmfSwZ1VLBNGhQ==, tarball: file:projects/collaborator.tgz}
version: 0.0.0
'@rush-temp/contact-assets@file:projects/contact-assets.tgz':
@@ -3965,11 +3975,11 @@ packages:
version: 0.0.0
'@rush-temp/contact-resources@file:projects/contact-resources.tgz':
resolution: {integrity: sha512-sOoou1UaCHSN/qYHhEiWyiPICxmqP2p+ACDAeVzhU8fuUa5tejlzuANOdiEYqz4c8+u/NhlCQHd7G7G5WjMPpg==, tarball: file:projects/contact-resources.tgz}
resolution: {integrity: sha512-RWk6026zqPV98SklhNMT0+FjevdIJS6kI3vEf4Y3H2EuP9lFJuj5a2pTiWRXFT+yeoOkYrCtUD0duZ65JrJidw==, tarball: file:projects/contact-resources.tgz}
version: 0.0.0
'@rush-temp/contact@file:projects/contact.tgz':
resolution: {integrity: sha512-79dZ9DTczWnB5eBmX1XH8d/mw8RjbGeCK+ryNpym3Bh3kykq7RwuTeTRvIlPTzk+vkCCXzb34A/zSdaslTyfnw==, tarball: file:projects/contact.tgz}
resolution: {integrity: sha512-+QCS1ZJvdStvQoBY8jt91ycyK4CK+W9C0txRhys5R/BCdzPkhdgl8OTZn6k60Fes0T0zKl5ceX3plOJXVtu+Hw==, tarball: file:projects/contact.tgz}
version: 0.0.0
'@rush-temp/controlled-documents-assets@file:projects/controlled-documents-assets.tgz':
@@ -3977,7 +3987,7 @@ packages:
version: 0.0.0
'@rush-temp/controlled-documents-resources@file:projects/controlled-documents-resources.tgz':
resolution: {integrity: sha512-C5G4bSM6HruIz0Ttj++bAXyzlix4UfsgNP7kYw+wHslj3x2zb9Qa3zEFVOtZPeB2MhpMlmlsz2Bu4F2an1Jfxw==, tarball: file:projects/controlled-documents-resources.tgz}
resolution: {integrity: sha512-FhuNC2ZQwCMkG9Jn2qfTOQf6b4Gh6CZnrzQI+cvr+6BK3uFaT3EnDMh2MlE2/lm0X7m98+d9T+2z9oFJmO5vYQ==, tarball: file:projects/controlled-documents-resources.tgz}
version: 0.0.0
'@rush-temp/controlled-documents@file:projects/controlled-documents.tgz':
@@ -4061,7 +4071,7 @@ packages:
version: 0.0.0
'@rush-temp/front@file:projects/front.tgz':
resolution: {integrity: sha512-dohiQMA5N6BQ+LJRe15hzxZOdP3edinNbP2id3xuoM/Xgq/Irs0EfczCo7zy3YcQocIDFMR71AYWGdXgilJbBw==, tarball: file:projects/front.tgz}
resolution: {integrity: sha512-nZ678oTwK/1srj53UHe1oNdkBUp3MrZdjxt0ptqXNkNWdPyFpQwe1PMinygBjU2+EmmygnCgvJTnQ+HtyWXtiQ==, tarball: file:projects/front.tgz}
version: 0.0.0
'@rush-temp/github-assets@file:projects/github-assets.tgz':
@@ -4097,7 +4107,7 @@ packages:
version: 0.0.0
'@rush-temp/guest-resources@file:projects/guest-resources.tgz':
resolution: {integrity: sha512-RfqwhU41ifAGcQchDRFYT12hz9wj0NsQ5ffr1vNeQg5+5s7nEu2o2lIy4jd6nvBpP5/b1d+QpObSmgJ/irvi1A==, tarball: file:projects/guest-resources.tgz}
resolution: {integrity: sha512-kCjOwB3F17OgPj2Op6erq7oht+o/HdR9hwiQtKtropjMf3iq6QALpK0ReHl1tIMpaTvd9MZLFQYmrpBX6BXwHw==, tarball: file:projects/guest-resources.tgz}
version: 0.0.0
'@rush-temp/guest@file:projects/guest.tgz':
@@ -4169,11 +4179,11 @@ packages:
version: 0.0.0
'@rush-temp/login-resources@file:projects/login-resources.tgz':
resolution: {integrity: sha512-NARTLQpe+poR8P/l3cOTAEPFutkH24EQqjw6zY6pIChSPhBVtK4GJu4yRJo7WtZPjBTq8qL7q3bDoZ7+aooAWw==, tarball: file:projects/login-resources.tgz}
resolution: {integrity: sha512-n8jvuqgHgissnRkZBIXW6EP6zXR66DrwC4vVG6+DV2rMmZq25W/PBT9mam+lwFo/L0DcQ5sBGerhyDrLbjq64w==, tarball: file:projects/login-resources.tgz}
version: 0.0.0
'@rush-temp/login@file:projects/login.tgz':
resolution: {integrity: sha512-jX5cfakMhaPs02qHZsnzTtFknqMhyz1mI7Wqd0NLthHsSfVP9NIbnB/DhUG9hChVm3Szj7cBrXg+skvv0aDCSQ==, tarball: file:projects/login.tgz}
resolution: {integrity: sha512-V3NnYilBtJGxpAF6i9WDcIlvqzmy84XaRuv0ekKYxIUYjQGGLaKRGs5cwONvsBgm02ZJ9VGh9nqGV5S3YPUTBg==, tarball: file:projects/login.tgz}
version: 0.0.0
'@rush-temp/love-assets@file:projects/love-assets.tgz':
@@ -4509,7 +4519,7 @@ packages:
version: 0.0.0
'@rush-temp/model@file:projects/model.tgz':
resolution: {integrity: sha512-wnsVHHgMBB73uVsYY5S/UigD6u0BDFVUpirCToSOGy8IkQR5Q9YKjgiyAX3K+PFkBY7VaL1a/QoqWZEQCy97ow==, tarball: file:projects/model.tgz}
resolution: {integrity: sha512-MUWU7orOyyibYO9aRQaElxt3VrQFOHbr5qtC9sJYkp9F4PNEuM+/1aYUALDJiuqo14BleVOcNV28030yBXv8CA==, tarball: file:projects/model.tgz}
version: 0.0.0
'@rush-temp/mongo@file:projects/mongo.tgz':
@@ -4565,7 +4575,7 @@ packages:
version: 0.0.0
'@rush-temp/pod-analytics-collector@file:projects/pod-analytics-collector.tgz':
resolution: {integrity: sha512-KuE4/ZuLMoEOqY2QyXv7AD0uPt5J7Uwt6jP70iWXcR2oET1+BhD2QTm/9MNsgOsnfiqp0Tdv06pfu7fb2A2Gcw==, tarball: file:projects/pod-analytics-collector.tgz}
resolution: {integrity: sha512-eCAWgfNhBAyArb1VyMKjMMCzWsoQcocLxyfurr6hM13JPqMMuK7no9z1MGHjd+mPkcvfpRGGh5I03XaPSe9gyg==, tarball: file:projects/pod-analytics-collector.tgz}
version: 0.0.0
'@rush-temp/pod-backup@file:projects/pod-backup.tgz':
@@ -4589,7 +4599,7 @@ packages:
version: 0.0.0
'@rush-temp/pod-github@file:projects/pod-github.tgz':
resolution: {integrity: sha512-FPRCZ36WCkBXnk5ATqZKSIq0KfPHT0mYZpvpzLw1z5J74osXHxMoifo28Lb7bZcHDbXg6o6VOGHotBvPgCYUng==, tarball: file:projects/pod-github.tgz}
resolution: {integrity: sha512-Szu3mv8d6NPVJtTXbnxK4LvO0yCMhJeOjOxX/hQNjizR4bzuWA0RFmihTUxGiByqWhuwciVC5G7ZtjZdprATzQ==, tarball: file:projects/pod-github.tgz}
version: 0.0.0
'@rush-temp/pod-gmail@file:projects/pod-gmail.tgz':
@@ -4597,11 +4607,11 @@ packages:
version: 0.0.0
'@rush-temp/pod-love@file:projects/pod-love.tgz':
resolution: {integrity: sha512-G70rzmJHk/en1EdQHEs2HEK9/D2IY0i/iF0WZtU2FWLtSzlJb5UC0hrpVfxD/aWcnfmmqcLQu+b2siYrnUFSmQ==, tarball: file:projects/pod-love.tgz}
resolution: {integrity: sha512-m+9dPMR45FJpQB+M5ka4PiJcTpV8COlQA5F36is2LkMLwnsDjcB94Sx+TFdIyjhVFpfB8l+t886mSPveHql4ag==, tarball: file:projects/pod-love.tgz}
version: 0.0.0
'@rush-temp/pod-print@file:projects/pod-print.tgz':
resolution: {integrity: sha512-uH6mY0Z3/3bbAe+rGjr2WPJmavKnoNgKZ5KoLqToGGF+iDzZOZGLjd8OrwUI90Z3behW7PPIRuPDGiRmFGOtcw==, tarball: file:projects/pod-print.tgz}
resolution: {integrity: sha512-i0Y9ShIeieNqCNMCXax1CZTStiDTw9M156m7fCDSrNS6VV4v2e5jy/HyNLtpyXz/ZRlOKndBdyiKsls+5pDL8g==, tarball: file:projects/pod-print.tgz}
version: 0.0.0
'@rush-temp/pod-server@file:projects/pod-server.tgz':
@@ -4613,7 +4623,7 @@ packages:
version: 0.0.0
'@rush-temp/pod-sign@file:projects/pod-sign.tgz':
resolution: {integrity: sha512-2CvZ6MihYZ/0DXWllTffUrvNyRjLJvvUkcT36EpVmgydo2F4guxAhrnsyaivxNPNwP8NuSy7JJ4uXXOYNeXCQg==, tarball: file:projects/pod-sign.tgz}
resolution: {integrity: sha512-CmOWryDSvF5qg0NTlkxT29gZP0qj0DimD25X2U6sQGtowtEytNvVmmg3cuSNTpwN+WR6+eli4JWyK6gathi+fQ==, tarball: file:projects/pod-sign.tgz}
version: 0.0.0
'@rush-temp/pod-stats@file:projects/pod-stats.tgz':
@@ -4793,7 +4803,7 @@ packages:
version: 0.0.0
'@rush-temp/server-calendar-resources@file:projects/server-calendar-resources.tgz':
resolution: {integrity: sha512-Rpdy7Nx56rXrJ1apUsvlancAVa5USFSwXboZ8iexTJ4Jaebv1yf32j5LUo/fLKwpZyL9KaUHT+A4N+nJKoRVWg==, tarball: file:projects/server-calendar-resources.tgz}
resolution: {integrity: sha512-92TFFDbAxCmUu4V3IaNDOLhbEEqTCB5dRohzpV1nzmroO8r+JneZjdV1j8OPHJF65ml0uqciDFPOct+kMzx5fg==, tarball: file:projects/server-calendar-resources.tgz}
version: 0.0.0
'@rush-temp/server-calendar@file:projects/server-calendar.tgz':
@@ -4801,7 +4811,7 @@ packages:
version: 0.0.0
'@rush-temp/server-chunter-resources@file:projects/server-chunter-resources.tgz':
resolution: {integrity: sha512-anzs0C7AXicSpf9F+jjqdokc3cZunw5O8gC0095fQmEoOW94OmvTyboGrx8iK3i+iYam4Z6Bkt60rXRo3JvXZQ==, tarball: file:projects/server-chunter-resources.tgz}
resolution: {integrity: sha512-kiR/o8nysn7cgV4YGZNzxBpqzpoTqXGPyF7fXO3MJ0aZPa46AWQPushZMQTnR9O7wFs3RUuHR7Bc6VsjlzqDqA==, tarball: file:projects/server-chunter-resources.tgz}
version: 0.0.0
'@rush-temp/server-chunter@file:projects/server-chunter.tgz':
@@ -4809,7 +4819,7 @@ packages:
version: 0.0.0
'@rush-temp/server-client@file:projects/server-client.tgz':
resolution: {integrity: sha512-ixXx7EdCjt/KMKk6pVedAj/27hWqDwCBnHhmTJc694OR8wTEHyIaohP9t8gcjka/ugJRJJjOmAsF1sd8HphW4g==, tarball: file:projects/server-client.tgz}
resolution: {integrity: sha512-uJeov8Q516lmcb4ZfIRdibl6PHP+INgobu/cjGIJ2WyOQaj2qBiABAq0IehGFF6Vb9EGXbQTB2VUuDvbabWEBQ==, tarball: file:projects/server-client.tgz}
version: 0.0.0
'@rush-temp/server-collaboration-resources@file:projects/server-collaboration-resources.tgz':
@@ -4821,15 +4831,15 @@ packages:
version: 0.0.0
'@rush-temp/server-contact-resources@file:projects/server-contact-resources.tgz':
resolution: {integrity: sha512-uBxxy/nVfaqq3u6dPETKCjoQOHCJDRGQFjaau9Lf2ThmJ7ZdxxIZcJ05CfJD4OSgQ1vgf13ZEB4OkDpzqQM5mw==, tarball: file:projects/server-contact-resources.tgz}
resolution: {integrity: sha512-6R2S1cbwLtDKCesTPG9tJVy7mJB6EJogkf8TDAv9RFPRqxBkzdQh6OlgkLQsRMgpuneoJjwmSxXFHhX3btVnug==, tarball: file:projects/server-contact-resources.tgz}
version: 0.0.0
'@rush-temp/server-contact@file:projects/server-contact.tgz':
resolution: {integrity: sha512-wH/4AxAjJUi/wU5hR5gbRK1k46L4FW5ydkphun1gbkO/GtNeSSdYAU48PfxJ6MkHTp+Hn0Ww7C5UxHfHjFOjAw==, tarball: file:projects/server-contact.tgz}
resolution: {integrity: sha512-3mD30C7Q8n6xsDge6A0OHJUS6I+9gRI/mw8LyhI98B04o/wJ81fIAM4Fmg6hRPmL1hvgylkNr55Ui1IQr5Cksw==, tarball: file:projects/server-contact.tgz}
version: 0.0.0
'@rush-temp/server-controlled-documents-resources@file:projects/server-controlled-documents-resources.tgz':
resolution: {integrity: sha512-atiXw3GqCij5FY7fbz+ZGWnfGVMrQmGOURz34OQbNGVoZRFVimUn7pdMApsU0MP3JZBWDEtairZYPGh2nvs5SA==, tarball: file:projects/server-controlled-documents-resources.tgz}
resolution: {integrity: sha512-OWXWbWiL83nG0kcP9KV1F8PXFxN/YqDaB5NVTmZA9YUwAJpjxNjtHK7VEsdcvldxOiJsi/aDX5vueIjtgYhK4Q==, tarball: file:projects/server-controlled-documents-resources.tgz}
version: 0.0.0
'@rush-temp/server-controlled-documents@file:projects/server-controlled-documents.tgz':
@@ -4893,7 +4903,7 @@ packages:
version: 0.0.0
'@rush-temp/server-hr-resources@file:projects/server-hr-resources.tgz':
resolution: {integrity: sha512-dRJrMwYzzZFaZ64iHkfaKQjP4EAnbFTn9HonG2w59T0KBGSk1cQlldj1G0DObovPhgl3bTcTJyiFTZo8sEKqVQ==, tarball: file:projects/server-hr-resources.tgz}
resolution: {integrity: sha512-sKDUqKJLI+KIEHICNUuvsHH5aGiVd2dOODfwoKmroe3y2bGYFcwHqFmWhWXCX9UyJ5ovfLdh/niY3Bqtx5V26w==, tarball: file:projects/server-hr-resources.tgz}
version: 0.0.0
'@rush-temp/server-hr@file:projects/server-hr.tgz':
@@ -4921,7 +4931,7 @@ packages:
version: 0.0.0
'@rush-temp/server-love-resources@file:projects/server-love-resources.tgz':
resolution: {integrity: sha512-v5wtIer348LTlHf515DVi+LYLwL2iWTmHWpVt0IRGZG459gmW0Sge546mo9Nt2N64uACSWW+LVza6x6rDqzVEQ==, tarball: file:projects/server-love-resources.tgz}
resolution: {integrity: sha512-C+m2Gxq+YZ2PBgCWEH8C4dWcFeWNwWha7rP5ugPzmt4E6InyZBBUVZD36j4hYJpVIiffWUvjJ5Qm9ImO4zplbw==, tarball: file:projects/server-love-resources.tgz}
version: 0.0.0
'@rush-temp/server-love@file:projects/server-love.tgz':
@@ -4929,7 +4939,7 @@ packages:
version: 0.0.0
'@rush-temp/server-notification-resources@file:projects/server-notification-resources.tgz':
resolution: {integrity: sha512-sLf+6eAqeGkx+28mefySh7RB9mMnEh5Cuw/Q036PALbzj/1tihPIIxlst9kMZAYgGr7WRaZGDGK4Pq2yxfCrPQ==, tarball: file:projects/server-notification-resources.tgz}
resolution: {integrity: sha512-XfvqNRHjygDUvXhwdvyQ3w+lp3m1b1iE/p2vXBiylkk54PJU8I0Wot6rJEwnSYIUaPM6ZnvjL5ROoQDYrSue8w==, tarball: file:projects/server-notification-resources.tgz}
version: 0.0.0
'@rush-temp/server-notification@file:projects/server-notification.tgz':
@@ -5001,7 +5011,7 @@ packages:
version: 0.0.0
'@rush-temp/server-time-resources@file:projects/server-time-resources.tgz':
resolution: {integrity: sha512-0arrBFlOQYDRxE3vokplD4aQt7KQ9ucaCbex949FJ+hLrjZClfVs5myB9+rzD4NSHX4AAceIzeHLhF5hKYmf+Q==, tarball: file:projects/server-time-resources.tgz}
resolution: {integrity: sha512-sIJIhhjtHcHXQgRvN2ek6YUOjcxrAakYD9UUBU4R07lzC+tKpyB5ypeWBweZuiRN9DLdAQKMwgijm4ez8Osb+A==, tarball: file:projects/server-time-resources.tgz}
version: 0.0.0
'@rush-temp/server-time@file:projects/server-time.tgz':
@@ -5013,11 +5023,11 @@ packages:
version: 0.0.0
'@rush-temp/server-tool@file:projects/server-tool.tgz':
resolution: {integrity: sha512-AdfPlf8gfUG+0JXgz9ZqM2Bt/BwN7ghTniXpM/dAtSIiJds2MDNkzpkvgOYu5jy0MjUjxlC1oivI8eg6TNRlpQ==, tarball: file:projects/server-tool.tgz}
resolution: {integrity: sha512-XGLZjlEMa+MBHaEr8yUT/c2Rn41CSRIW6ihPJSwA9T0hYn+vkoIXKXV/n7+BnpomXBY58zxlOQE1rSYbBKrtDA==, tarball: file:projects/server-tool.tgz}
version: 0.0.0
'@rush-temp/server-tracker-resources@file:projects/server-tracker-resources.tgz':
resolution: {integrity: sha512-Npz6QfpPLP18rNxIGzNvjKf4QEhT8ywVdlYNt7e4QGvA1zetFDava2Cg0QpN31LZue8pTDCC3T94kBQMUiUfqg==, tarball: file:projects/server-tracker-resources.tgz}
resolution: {integrity: sha512-K6rsjo4rNef5+c7LhGFy+mwfwRb4b6YnwRzfP38gveTa0MA5CdMGTlmnaE5/OEciUwCRg77ePOyjA3giW/V1AA==, tarball: file:projects/server-tracker-resources.tgz}
version: 0.0.0
'@rush-temp/server-tracker@file:projects/server-tracker.tgz':
@@ -5041,11 +5051,11 @@ packages:
version: 0.0.0
'@rush-temp/server-ws@file:projects/server-ws.tgz':
resolution: {integrity: sha512-9PP7kE8LXoZ+7r6OwLp26KT+JFAQtSwg5QP/awwXjcsV2t3yphDYKbdAJuVG5xlLfHW52Xn4NRbr0wFhfwS0sg==, tarball: file:projects/server-ws.tgz}
resolution: {integrity: sha512-5g3NGK0JdwcpXIRpsGN75fiev+4fpwl4V33V5rwzrLZzHUNE7CAzTtc/cFRxK5cZIVRwaaTNCYi+WCEfoFI7Tg==, tarball: file:projects/server-ws.tgz}
version: 0.0.0
'@rush-temp/server@file:projects/server.tgz':
resolution: {integrity: sha512-PwdTpzGoD5SYmDTSD4hJT7ZkG2b05H030bVz5I+vz5f/AmgAzowxfxa0pbMujI6tEiU+o6hDiK25PQXn2xduHA==, tarball: file:projects/server.tgz}
resolution: {integrity: sha512-378GorQHM5ihU3dZLis+aq/JRjXeIgVOzIV/W3k27KB8EzoZGDbZQbDuKC07vAPxNBaoJ5yYcLTdGWx6XHnwzw==, tarball: file:projects/server.tgz}
version: 0.0.0
'@rush-temp/setting-assets@file:projects/setting-assets.tgz':
@@ -5053,7 +5063,7 @@ packages:
version: 0.0.0
'@rush-temp/setting-resources@file:projects/setting-resources.tgz':
resolution: {integrity: sha512-XudJm31jzp8dEwS9TsP8lpxB2/yYjvafLULunIlgG46LdhQBfDG711m9w9gLb41Jj9O1quj2egHI87fHfmwrrg==, tarball: file:projects/setting-resources.tgz}
resolution: {integrity: sha512-f1+pi6AyYD3Vr5UbZfqt4sOwCRC3ZL5x3EQBDUqD+XKx/XbVfCMF3fp7E1Jw+tA3v/Rp+b7w+DBUQnxa4nVRDQ==, tarball: file:projects/setting-resources.tgz}
version: 0.0.0
'@rush-temp/setting@file:projects/setting.tgz':
@@ -5257,7 +5267,7 @@ packages:
version: 0.0.0
'@rush-temp/workbench-resources@file:projects/workbench-resources.tgz':
resolution: {integrity: sha512-315FeO65RF4AACdkoe6rPuva+WRyhF48FvyYLOg558Te9tZqwEvKjwR/KWrgZPRkm8mcemw9xWBhirfO9Grkqw==, tarball: file:projects/workbench-resources.tgz}
resolution: {integrity: sha512-wnOyuF9Gv2lBWbszQxpfxQ1fF15tG9toi26+Bt8IONSejZsaDrQw/rXhZsW9wgWbtoRnD17Gjg5G2YUPzWZvUg==, tarball: file:projects/workbench-resources.tgz}
version: 0.0.0
'@rush-temp/workbench@file:projects/workbench.tgz':
@@ -5265,7 +5275,7 @@ packages:
version: 0.0.0
'@rush-temp/workspace-service@file:projects/workspace-service.tgz':
resolution: {integrity: sha512-Rj6gppjTr+cKVJqzPxQ7PZy4reEXSEvjJouyZb7e3NApoh7WtqBWYau+N9i/jQw5C1dyGcH3HHaRBoFH9ZZcHA==, tarball: file:projects/workspace-service.tgz}
resolution: {integrity: sha512-tQ49Q9CrzYAD3cibL+LuX1mf9lZ3kQwLJhviHhodOKqx9qmdrMCTBLt1DIJVC96AjblKta8Afa1Y2Wl8FIYhtw==, tarball: file:projects/workspace-service.tgz}
version: 0.0.0
'@selderee/plugin-htmlparser2@0.11.0':
@@ -6204,6 +6214,9 @@ packages:
'@types/pg-pool@2.0.6':
resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==}
'@types/pg@8.11.11':
resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==}
'@types/pg@8.6.1':
resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==}
@@ -11201,6 +11214,10 @@ packages:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
pg-numeric@1.0.2:
resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
engines: {node: '>=4'}
pg-protocol@1.7.0:
resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==}
@@ -11208,6 +11225,10 @@ packages:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
pg-types@4.0.2:
resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==}
engines: {node: '>=10'}
phin@2.9.3:
resolution: {integrity: sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
@@ -11376,18 +11397,37 @@ packages:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
postgres-array@3.0.2:
resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==}
engines: {node: '>=12'}
postgres-bytea@1.0.0:
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
engines: {node: '>=0.10.0'}
postgres-bytea@3.0.0:
resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==}
engines: {node: '>= 6'}
postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
postgres-date@2.1.0:
resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==}
engines: {node: '>=12'}
postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
postgres-interval@3.0.0:
resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==}
engines: {node: '>=12'}
postgres-range@1.1.4:
resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==}
postgres@3.4.5:
resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==}
engines: {node: '>=12'}
@@ -15711,6 +15751,32 @@ snapshots:
'@remirror/core-constants@3.0.0': {}
'@rush-temp/account-client@file:projects/account-client.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))':
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))(eslint@8.56.0)(typescript@5.7.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3)
cross-env: 7.0.3
esbuild: 0.24.2
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(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))(eslint@8.56.0)(typescript@5.7.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(@types/node@20.11.19)(typescript@5.3.3))
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.7.3)
typescript: 5.7.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- babel-jest
- babel-plugin-macros
- node-notifier
- supports-color
- ts-node
'@rush-temp/account-service@file:projects/account-service.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)':
dependencies:
'@koa/cors': 5.0.0
@@ -15757,12 +15823,11 @@ snapshots:
'@rush-temp/account@file:projects/account.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))':
dependencies:
'@types/crypto-js': 4.2.2
'@types/jest': 29.5.12
'@types/otp-generator': 4.0.2
'@types/pg': 8.11.11
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
crypto-js: 4.2.0
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(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))(eslint@8.56.0)(typescript@5.3.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
@@ -17070,6 +17135,7 @@ snapshots:
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
prettier: 3.2.5
svelte: 4.2.19
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
@@ -27033,7 +27099,13 @@ snapshots:
'@types/pg-pool@2.0.6':
dependencies:
'@types/pg': 8.6.1
'@types/pg': 8.11.11
'@types/pg@8.11.11':
dependencies:
'@types/node': 20.11.19
pg-protocol: 1.7.0
pg-types: 4.0.2
'@types/pg@8.6.1':
dependencies:
@@ -33237,6 +33309,8 @@ snapshots:
pg-int8@1.0.1: {}
pg-numeric@1.0.2: {}
pg-protocol@1.7.0: {}
pg-types@2.2.0:
@@ -33247,6 +33321,16 @@ snapshots:
postgres-date: 1.0.7
postgres-interval: 1.2.0
pg-types@4.0.2:
dependencies:
pg-int8: 1.0.1
pg-numeric: 1.0.2
postgres-array: 3.0.2
postgres-bytea: 3.0.0
postgres-date: 2.1.0
postgres-interval: 3.0.0
postgres-range: 1.1.4
phin@2.9.3: {}
phin@3.7.1:
@@ -33400,14 +33484,26 @@ snapshots:
postgres-array@2.0.0: {}
postgres-array@3.0.2: {}
postgres-bytea@1.0.0: {}
postgres-bytea@3.0.0:
dependencies:
obuf: 1.1.2
postgres-date@1.0.7: {}
postgres-date@2.1.0: {}
postgres-interval@1.2.0:
dependencies:
xtend: 4.0.2
postgres-interval@3.0.0: {}
postgres-range@1.1.4: {}
postgres@3.4.5: {}
posthog-js@1.122.0:
+6 -7
View File
@@ -1,5 +1,5 @@
import client, { clientId } from '@hcengineering/client'
import { getWorkspaceId, MeasureMetricsContext, type BackupClient, type Client } from '@hcengineering/core'
import { MeasureMetricsContext, WorkspaceIds, type BackupClient, type Client } from '@hcengineering/core'
import { addLocation, getResource, setMetadata } from '@hcengineering/platform'
import WebSocket from 'ws'
@@ -31,16 +31,15 @@ export async function createClient (
return (await clientFactory(token, transactorUrl)) as unknown as Client & BackupClient
}
async function doBackup (dirName: string, token: string, endpoint: string, workspace: string, notify: (command: string, ...args: any[]) => void, backupHugeFiles: boolean): Promise<void> {
async function doBackup (dirName: string, token: string, endpoint: string, wsIds: WorkspaceIds, notify: (command: string, ...args: any[]) => void, backupHugeFiles: boolean): Promise<void> {
notify('backup', 0)
const ctx = new MeasureMetricsContext('backup', {})
const storage = await createFileBackupStorage(dirName)
const wsid = getWorkspaceId(workspace)
const client = await createClient(endpoint, token)
try {
ctx.info('do backup', { workspace, endpoint })
await backup(ctx, endpoint, wsid, storage, {
ctx.info('do backup', { ...wsIds, endpoint })
await backup(ctx, endpoint, wsIds, storage, {
force: true,
freshBackup: false,
clean: false,
@@ -65,7 +64,7 @@ async function doBackup (dirName: string, token: string, endpoint: string, works
notify('backup', 100)
}
export function startBackup (window: BrowserWindow, token: string, endpoint: string, workspace: string, cmd: (command: string, ...args: any[]) => void): void {
export function startBackup (window: BrowserWindow, token: string, endpoint: string, wsIds: WorkspaceIds, cmd: (command: string, ...args: any[]) => void): void {
void dialog
.showOpenDialog(window, {
properties: ['openDirectory'],
@@ -94,7 +93,7 @@ export function startBackup (window: BrowserWindow, token: string, endpoint: str
runningBackup = {
notify: cmd
}
void doBackup(response.filePaths[0], token, endpoint, workspace, cmd, backupKind.checkboxChecked)
void doBackup(response.filePaths[0], token, endpoint, wsIds, cmd, backupKind.checkboxChecked)
})
}
})
+3 -3
View File
@@ -424,10 +424,10 @@ autoUpdater.on('update-downloaded', (info) => {
autoUpdater.quitAndInstall()
})
ipcMain.on('start-backup', (event, token, endpoint, workspace) => {
console.log('start backup', token, endpoint, workspace)
ipcMain.on('start-backup', (event, token, endpoint, wsIds) => {
console.log('start backup', token, endpoint, wsIds)
if (mainWindow != null) {
startBackup(mainWindow, token, endpoint, workspace, (cmd: string, ...args: any[]) => {
startBackup(mainWindow, token, endpoint, wsIds, (cmd: string, ...args: any[]) => {
mainWindow?.webContents.send(cmd, ...args)
})
}
+13 -5
View File
@@ -61,13 +61,14 @@ window.addEventListener('DOMContentLoaded', () => {
const tokens = fetchMetadataLocalStorage(login.metadata.LoginTokens)
if (tokens !== null) {
const loc = getCurrentLocation()
loc.path.splice(1, 1)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete tokens[loc.path[1]]
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
}
setMetadata(presentation.metadata.Token, null)
setMetadataLocalStorage(login.metadata.LastToken, null)
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
setMetadataLocalStorage(login.metadata.LoginEmail, null)
setMetadataLocalStorage(login.metadata.LoginAccount, null)
void closeClient().then(() => {
navigate({ path: [loginId] })
})
@@ -104,10 +105,17 @@ window.addEventListener('DOMContentLoaded', () => {
// We need to obtain current token and endpoint and trigger backup
const token = getMetadata(presentation.metadata.Token)
const endpoint = getMetadata(presentation.metadata.Endpoint)
const workspace = getMetadata(presentation.metadata.WorkspaceId)
const workspaceUuid = getMetadata(presentation.metadata.WorkspaceUuid)
// const workspaceDataId = getMetadata(presentation.metadata.WorkspaceDataId)
// const workspaceUrl = getMetadata(presentation.metadata.WorkspaceUrl)
// const wsIds = {
// uuid: workspaceUuid,
// dataId: workspaceDataId,
// url: workspaceUrl
// }
if (isOwnerOrMaintainer()) {
if (token != null && endpoint != null && workspace != null) {
// ipcMain.startBackup(token, endpoint, workspace)
if (token != null && endpoint != null && workspaceUuid != null) {
// ipcMain.startBackup(token, endpoint, wsIds)
closePopup()
closePanel()
const loc = getCurrentResolvedLocation()
+3 -10
View File
@@ -1,4 +1,4 @@
import contact, { PersonAccount, formatName } from '@hcengineering/contact'
import { getPersonBySocialId, formatName } from '@hcengineering/contact'
import { Ref, TxOperations } from '@hcengineering/core'
import notification, { DocNotifyContext, CommonInboxNotification, ActivityInboxNotification, InboxNotification } from '@hcengineering/notification'
import { IntlString, addEventListener, translate } from '@hcengineering/platform'
@@ -67,14 +67,7 @@ async function hydrateNotificationAsYouCan (lastNotification: InboxNotification)
body: ''
}
const account = await client.getModel().findOne(contact.class.PersonAccount, { _id: lastNotification.modifiedBy as Ref<PersonAccount> })
if (account == null) {
return noPersonData
}
const person = await client.findOne(contact.class.Person, { _id: account.person })
const person = await getPersonBySocialId(client, lastNotification.modifiedBy)
if (person == null) {
return noPersonData
}
@@ -122,7 +115,7 @@ export function configureNotifications (): void {
let initTimestamp = 0
const notificationHistory = new Map<string, number>()
addEventListener(workbench.event.NotifyConnection, async (event, account: PersonAccount) => {
addEventListener(workbench.event.NotifyConnection, async () => {
client = getClient()
const electronAPI: IPCMainExposed = (window as any).electron
+1 -1
View File
@@ -340,7 +340,7 @@ export async function configurePlatform (): Promise<void> {
initThemeStore()
addEventListener(workbench.event.NotifyConnection, async (evt) => {
addEventListener(workbench.event.NotifyConnection, async () => {
await ipcMain.setFrontCookie(
config.FRONT_URL,
presentation.metadata.Token.replaceAll(':', '-'),
+1 -1
View File
@@ -145,6 +145,6 @@ const expose: IPCMainExposed = {
getScreenAccess: () => ipcRenderer.invoke('get-screen-access'),
getScreenSources: () => ipcRenderer.invoke('get-screen-sources'),
cancelBackup: () => { ipcRenderer.send('cancel-backup') },
startBackup: (token, endpoint, workspace) => { ipcRenderer.send('start-backup', token, endpoint, workspace) }
startBackup: (token, endpoint, wsIds) => { ipcRenderer.send('start-backup', token, endpoint, wsIds) }
}
contextBridge.exposeInMainWorld('electron', expose)
+3 -2
View File
@@ -1,5 +1,5 @@
import { Employee } from '@hcengineering/contact'
import { Ref, WorkspaceId } from '@hcengineering/core'
import { Ref, WorkspaceDataId, WorkspaceUuid } from '@hcengineering/core'
import { DocumentSpace } from '@hcengineering/controlled-documents'
import { StorageAdapter } from '@hcengineering/server-core'
@@ -11,7 +11,8 @@ export interface Config {
collaborator?: string
collaboratorURL: string
uploadURL: string
workspaceId: WorkspaceId
workspaceId: WorkspaceUuid
workspaceDataId?: WorkspaceDataId
owner: Ref<Employee>
backend: HtmlConversionBackend
space: Ref<DocumentSpace>
+13 -5
View File
@@ -20,9 +20,10 @@ import core, {
MeasureContext,
Ref,
TxOperations,
type WorkspaceDataId,
generateId,
makeDocCollabId,
systemAccountEmail,
systemAccountUuid,
type Blob
} from '@hcengineering/core'
import { createClient, getTransactorEndpoint } from '@hcengineering/server-client'
@@ -40,9 +41,9 @@ export default async function importExtractedFile (
extractedFile: ExtractedFile
): Promise<void> {
const { workspaceId } = config
const token = generateToken(systemAccountEmail, workspaceId)
const token = generateToken(systemAccountUuid, workspaceId)
const transactorUrl = await getTransactorEndpoint(token, 'external')
console.log(`Connecting to transactor: ${transactorUrl} (ws: '${workspaceId.name}')`)
console.log(`Connecting to transactor: ${transactorUrl} (ws: '${workspaceId}')`)
const connection = (await createClient(transactorUrl, token)) as CoreClient & BackupClient
try {
@@ -252,7 +253,7 @@ export async function processImages (
const dom = parseDocument(section.content)
const imageNodes = findAll((n) => n.tagName === 'img', dom.children)
const { storageAdapter, workspaceId, uploadURL } = config
const { storageAdapter, workspaceId, workspaceDataId, uploadURL } = config
const imageUploads = imageNodes.map(async (img) => {
const src = img.attribs.src
@@ -272,7 +273,14 @@ export async function processImages (
// upload
const uuid = generateId()
await storageAdapter.put(ctx, workspaceId, uuid, fileContents, mimeType, fileSize)
await storageAdapter.put(
ctx,
workspaceDataId ?? (workspaceId as unknown as WorkspaceDataId),
uuid,
fileContents,
mimeType,
fileSize
)
// attachment
const attachmentId: Ref<Attachment> = generateId()
+7 -5
View File
@@ -14,7 +14,7 @@
//
import { Employee } from '@hcengineering/contact'
import documents, { DocumentSpace } from '@hcengineering/controlled-documents'
import { MeasureMetricsContext, Ref, getWorkspaceId, systemAccountEmail } from '@hcengineering/core'
import { MeasureMetricsContext, Ref, systemAccountUuid, WorkspaceUuid, type WorkspaceDataId } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform'
import serverClientPlugin from '@hcengineering/server-client'
import { type StorageAdapter } from '@hcengineering/server-core'
@@ -74,13 +74,14 @@ export function docImportTool (): void {
.description('import doc into workspace')
.option('-s|--spec <spec>', 'Specification file')
.option('-b|--backend <backend>', 'Conversion backend', 'pandoc')
.option('-d|--dataId <workspaceDataId>', 'Workspace data ID')
.option('--space <space>', 'Doc space ID', documents.space.QualityDocuments)
.action(
async (
doc: string,
workspace: string,
workspace: WorkspaceUuid,
owner: Ref<Employee>,
cmd: { backend: string, space: Ref<DocumentSpace>, spec?: string }
cmd: { backend: string, workspaceDataId?: WorkspaceDataId, space: Ref<DocumentSpace>, spec?: string }
) => {
console.log(
`Importing document '${doc}' into workspace '${workspace}', owner: ${JSON.stringify(owner)}, spec: ${
@@ -89,11 +90,12 @@ export function docImportTool (): void {
)
await withStorage(async (storageAdapter) => {
const workspaceId = getWorkspaceId(workspace)
const workspaceId = workspace
const config: Config = {
doc,
workspaceId,
workspaceDataId: cmd.workspaceDataId,
owner,
backend: getBackend(cmd.backend),
specFile: cmd.spec,
@@ -102,7 +104,7 @@ export function docImportTool (): void {
storageAdapter,
collaboratorURL: collaboratorUrl,
collaborator,
token: generateToken(systemAccountEmail, workspaceId)
token: generateToken(systemAccountUuid, workspaceId, { service: 'import-tool' })
}
await importDoc(ctx, config)
+6 -99
View File
@@ -19,18 +19,6 @@ services:
ports:
- 27017:27017
restart: unless-stopped
postgres:
image: postgres
container_name: postgres
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- POSTGRES_PASSWORD=example
volumes:
- dbpg:/data/db
ports:
- 5432:5432
restart: unless-stopped
cockroach:
image: cockroachdb/cockroach:latest-v24.2
ports:
@@ -86,11 +74,11 @@ services:
- ACCOUNT_PORT=3000
- SERVER_SECRET=secret
- STATS_URL=http://host.docker.internal:4900
# - DB_URL=postgresql://postgres:example@postgres:5432
# - DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable
- DB_URL=${MONGO_URL}
# - DB_NS=account-2
# Pass only one region to disallow selection for new workspaces.Ø
- REGION_INFO=|Mongo;pg|Postgres;cockroach|CockroachDB
- REGION_INFO=|Mongo;cockroach|CockroachDB
# - REGION_INFO=cockroach|CockroachDB
- TRANSACTOR_URL=ws://host.docker.internal:3333,ws://host.docker.internal:3331;;pg,ws://host.docker.internal:3332;;cockroach,
- SES_URL=
@@ -142,34 +130,6 @@ services:
- BACKUP_STORAGE=${BACKUP_STORAGE_CONFIG}
- BACKUP_BUCKET=${BACKUP_BUCKET_NAME}
restart: unless-stopped
workspacepg:
image: hardcoreeng/workspace
extra_hosts:
- 'host.docker.internal:host-gateway'
links:
- postgres
- minio
- stats
volumes:
- ./branding.json:/var/cfg/branding.json
environment:
- WS_OPERATION=all+backup
- SERVER_SECRET=secret
- DB_URL=postgresql://postgres:example@postgres:5432
- STATS_URL=http://host.docker.internal:4900
- SES_URL=
- REGION=pg
- STORAGE_CONFIG=${STORAGE_CONFIG}
- FRONT_URL=http://host.docker.internal:8087
- RESERVED_DB_NAMES=telegram,gmail,github
- MODEL_ENABLED=*
- ACCOUNTS_URL=http://host.docker.internal:3000
- BRANDING_PATH=/var/cfg/branding.json
# - PARALLEL=2
# - INIT_WORKSPACE=onboarding
- BACKUP_STORAGE=${BACKUP_STORAGE_CONFIG}
- BACKUP_BUCKET=${BACKUP_BUCKET_NAME}
restart: unless-stopped
workspace_cockroach:
image: hardcoreeng/workspace
extra_hosts:
@@ -286,43 +246,9 @@ services:
- ACCOUNTS_URL=http://host.docker.internal:3000
- LAST_NAME_FIRST=true
- BRANDING_PATH=/var/cfg/branding.json
- SUPPORT_WORKSPACE=support
- SUPPORT_WORKSPACE=09b65664-b3c3-4ea8-b9a1-c9688bde17f0
- AI_BOT_URL=http://host.docker.internal:4010
restart: unless-stopped
transactor_pg:
image: hardcoreeng/transactor
extra_hosts:
- 'host.docker.internal:host-gateway'
links:
- postgres
- minio
- account
- stats
# - apm-server
ports:
- 3331:3331
volumes:
- ./branding.json:/var/cfg/branding.json
environment:
# - SERVER_PROVIDER=uweb
# - UWS_HTTP_MAX_HEADERS_SIZE="32768"
# - UV_THREADPOOL_SIZE=10
- SERVER_PORT=3331
- SERVER_SECRET=secret
- ENABLE_COMPRESSION=true
- FULLTEXT_URL=http://host.docker.internal:4701
- STATS_URL=http://host.docker.internal:4900
- DB_URL=postgresql://postgres:example@postgres:5432
- METRICS_CONSOLE=false
- METRICS_FILE=metrics.txt
- STORAGE_CONFIG=${STORAGE_CONFIG}
- FRONT_URL=http://host.docker.internal:8087
# - APM_SERVER_URL=http://apm-server:8200
- SES_URL=''
- ACCOUNTS_URL=http://host.docker.internal:3000
- LAST_NAME_FIRST=true
- BRANDING_PATH=/var/cfg/branding.json
restart: unless-stopped
transactor_cockroach:
image: hardcoreeng/transactor
extra_hosts:
@@ -396,26 +322,6 @@ services:
- STATS_URL=http://host.docker.internal:4900
- REKONI_URL=http://host.docker.internal:4004
- ACCOUNTS_URL=http://host.docker.internal:3000
fulltext_pg:
image: hardcoreeng/fulltext
extra_hosts:
- 'host.docker.internal:host-gateway'
restart: unless-stopped
links:
- elastic
- postgres
ports:
- 4701:4701
environment:
- PORT=4701
- SERVER_SECRET=secret
- DB_URL=postgresql://postgres:example@postgres:5432
- FULLTEXT_DB_URL=http://host.docker.internal:9200
- ELASTIC_INDEX_NAME=local_storage_index
- STORAGE_CONFIG=${STORAGE_CONFIG}
- STATS_URL=http://host.docker.internal:4900
- REKONI_URL=http://host.docker.internal:4004
- ACCOUNTS_URL=http://host.docker.internal:3000
fulltext_cockroach:
image: hardcoreeng/fulltext
extra_hosts:
@@ -447,6 +353,7 @@ services:
- SECRET=secret
- STORAGE_CONFIG=${STORAGE_CONFIG}
- STATS_URL=http://host.docker.internal:4900
- ACCOUNTS_URL=http://host.docker.internal:3000
sign:
image: hardcoreeng/sign
extra_hosts:
@@ -481,7 +388,7 @@ services:
- 'MONGO_OPTIONS={"appName":"analytics","maxPoolSize":1}'
- SERVICE_ID=analytics-collector-service
- ACCOUNTS_URL=http://host.docker.internal:3000
- SUPPORT_WORKSPACE=support
- SUPPORT_WORKSPACE=09b65664-b3c3-4ea8-b9a1-c9688bde17f0
- STATS_URL=http://host.docker.internal:4900
aiBot:
image: hardcoreeng/ai-bot
@@ -494,7 +401,7 @@ services:
- SERVER_SECRET=secret
- MONGO_URL=${MONGO_URL}
- ACCOUNTS_URL=http://host.docker.internal:3000
- SUPPORT_WORKSPACE=support
- SUPPORT_WORKSPACE=09b65664-b3c3-4ea8-b9a1-c9688bde17f0
- STORAGE_CONFIG=${STORAGE_CONFIG}
- FIRST_NAME=Jolie
- LAST_NAME=AI
+16 -19
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { concatLink, TxOperations } from '@hcengineering/core'
import { buildSocialIdString, concatLink, SocialIdType, TxOperations } from '@hcengineering/core'
import {
ClickupImporter,
defaultDocumentPreprocessors,
@@ -25,12 +25,7 @@ import {
type Logger
} from '@hcengineering/importer'
import { setMetadata } from '@hcengineering/platform'
import serverClientPlugin, {
createClient,
getUserWorkspaces,
login,
selectWorkspace
} from '@hcengineering/server-client'
import serverClientPlugin, { createClient, getAccountClient } from '@hcengineering/server-client'
import { program } from 'commander'
import { readFileSync } from 'fs'
import * as yaml from 'js-yaml'
@@ -79,32 +74,34 @@ export function importTool (): void {
console.log('Setting up Accounts URL: ', config.ACCOUNTS_URL)
setMetadata(serverClientPlugin.metadata.Endpoint, config.ACCOUNTS_URL)
console.log('Trying to login user: ', user)
const userToken = await login(user, password, workspaceUrl)
if (userToken === undefined) {
const unauthAccountClient = getAccountClient()
const { account, token } = await unauthAccountClient.login(user, password)
if (token === undefined || account === undefined) {
console.log('Login failed for user: ', user)
return
}
console.log('Looking for workspace: ', workspaceUrl)
const allWorkspaces = await getUserWorkspaces(userToken)
const workspaces = allWorkspaces.filter((ws) => ws.workspaceUrl === workspaceUrl)
const accountClient = getAccountClient(token)
const allWorkspaces = await accountClient.getUserWorkspaces()
const workspaces = allWorkspaces.filter((ws) => ws.url === workspaceUrl)
if (workspaces.length < 1) {
console.log('Workspace not found: ', workspaceUrl)
return
}
console.log('Workspace found')
const selectedWs = await selectWorkspace(userToken, workspaces[0].workspace)
const selectedWs = await accountClient.selectWorkspace(workspaces[0].url)
console.log(selectedWs)
console.log('Connecting to Transactor URL: ', selectedWs.endpoint)
const connection = await createClient(selectedWs.endpoint, selectedWs.token)
const acc = connection.getModel().getAccountByEmail(user)
if (acc === undefined) {
console.log('Account not found for email: ', user)
return
}
const client = new TxOperations(connection, acc._id)
const fileUploader = new FrontFileUploader(getFrontUrl(), selectedWs.workspaceId, selectedWs.token)
const client = new TxOperations(connection, buildSocialIdString({ type: SocialIdType.EMAIL, value: user }))
const fileUploader = new FrontFileUploader(
getFrontUrl(),
selectedWs.workspace,
selectedWs.workspaceDataId ?? selectedWs.workspace,
selectedWs.token
)
try {
await f(client, fileUploader)
} catch (err: any) {
+2 -2
View File
@@ -193,7 +193,7 @@ services:
- 'MONGO_OPTIONS={"appName":"analytics","maxPoolSize":1}'
- SERVICE_ID=analytics-collector-service
- ACCOUNTS_URL=http://account:3000
- SUPPORT_WORKSPACE=support
- SUPPORT_WORKSPACE=09b65664-b3c3-4ea8-b9a1-c9688bde17f0
deploy:
resources:
limits:
@@ -205,7 +205,7 @@ services:
- SERVER_SECRET=secret
- MONGO_URL=mongodb://host.docker.internal:27017
- ACCOUNTS_URL=http://account:3000
- SUPPORT_WORKSPACE=support
- SUPPORT_WORKSPACE=09b65664-b3c3-4ea8-b9a1-c9688bde17f0
- FIRST_NAME=Jolie
- LAST_NAME=AI
- PASSWORD=password
+1
View File
@@ -64,6 +64,7 @@
"defaultApplication": "documents",
"defaultSpace": "documents:space:QualityDocuments",
"defaultSpecial": "",
"lastNameFirst": "true",
"links": [
{
"rel": "shortcut icon",
+2 -3
View File
@@ -20,9 +20,8 @@
"docker:abuild": "docker build -t hardcoreeng/tool . --platform=linux/arm64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/tool",
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/tool staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/tool",
"run-local": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --expose-gc --max-old-space-size=18000 ./bundle/bundle.js",
"run-local-pg": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=postgresql://postgres:example@localhost:5432 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --expose-gc --max-old-space-size=18000 ./bundle/bundle.js",
"run-local-cr": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --expose-gc --max-old-space-size=18000 ./bundle/bundle.js",
"run-local": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4700 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --expose-gc --max-old-space-size=18000 ./bundle/bundle.js",
"run-local-cr": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4702 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --expose-gc --max-old-space-size=18000 ./bundle/bundle.js",
"run-local-brk": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --inspect-brk --enable-source-maps --max-old-space-size=18000 ./bundle/bundle.js",
"run": "rush bundle --to @hcengineering/tool >/dev/null && cross-env node --max-old-space-size=8000 ./bundle/bundle.js",
"upgrade": "rushx run-local upgrade",
+175 -174
View File
@@ -1,202 +1,203 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
type Account,
type AccountDB,
changeEmail,
// changeEmail,
getAccount,
listWorkspacesPure,
// listWorkspacesPure,
type Workspace
} from '@hcengineering/account'
import core, { getWorkspaceId, type MeasureContext, systemAccountEmail, TxOperations } from '@hcengineering/core'
import core, { type MeasureContext, TxOperations } from '@hcengineering/core'
import contact from '@hcengineering/model-contact'
import { getTransactorEndpoint } from '@hcengineering/server-client'
import { generateToken } from '@hcengineering/server-token'
import { connect } from '@hcengineering/server-tool'
export async function renameAccount (
ctx: MeasureContext,
db: AccountDB,
accountsUrl: string,
oldEmail: string,
newEmail: string
): Promise<void> {
const account = await getAccount(db, oldEmail)
if (account == null) {
throw new Error("Account does'n exists")
}
// export async function renameAccount (
// ctx: MeasureContext,
// db: AccountDB,
// accountsUrl: string,
// oldEmail: string,
// newEmail: string
// ): Promise<void> {
// const account = await getAccount(db, oldEmail)
// if (account == null) {
// throw new Error("Account does'n exists")
// }
const newAccount = await getAccount(db, newEmail)
if (newAccount != null) {
throw new Error('New Account email already exists:' + newAccount?.email + ' ' + newAccount?._id?.toString())
}
// const newAccount = await getAccount(db, newEmail)
// if (newAccount != null) {
// throw new Error('New Account email already exists:' + newAccount?.email + ' ' + newAccount?._id?.toString())
// }
await changeEmail(ctx, db, account, newEmail)
// await changeEmail(ctx, db, account, newEmail)
await fixWorkspaceEmails(account, db, accountsUrl, oldEmail, newEmail)
}
// await fixWorkspaceEmails(account, db, accountsUrl, oldEmail, newEmail)
// }
export async function fixAccountEmails (
ctx: MeasureContext,
db: AccountDB,
transactorUrl: string,
oldEmail: string,
newEmail: string
): Promise<void> {
const account = await getAccount(db, newEmail)
if (account == null) {
throw new Error("Account does'n exists")
}
// export async function fixAccountEmails (
// ctx: MeasureContext,
// db: AccountDB,
// transactorUrl: string,
// oldEmail: string,
// newEmail: string
// ): Promise<void> {
// const account = await getAccount(db, newEmail)
// if (account == null) {
// throw new Error("Account does'n exists")
// }
await fixWorkspaceEmails(account, db, transactorUrl, oldEmail, newEmail)
}
async function fixWorkspaceEmails (
account: Account,
db: AccountDB,
accountsUrl: string,
oldEmail: string,
newEmail: string
): Promise<void> {
const accountWorkspaces = account.workspaces.map((it) => it.toString())
// We need to update all workspaces
const workspaces = await listWorkspacesPure(db)
for (const ws of workspaces) {
if (!accountWorkspaces.includes(ws._id.toString())) {
continue
}
console.log('checking workspace', ws.workspaceName, ws.workspace)
// await fixWorkspaceEmails(account, db, transactorUrl, oldEmail, newEmail)
// }
// async function fixWorkspaceEmails (
// account: Account,
// db: AccountDB,
// accountsUrl: string,
// oldEmail: string,
// newEmail: string
// ): Promise<void> {
// const accountWorkspaces = account.workspaces.map((it) => it.toString())
// // We need to update all workspaces
// const workspaces = await listWorkspacesPure(db)
// for (const ws of workspaces) {
// if (!accountWorkspaces.includes(ws._id.toString())) {
// continue
// }
// console.log('checking workspace', ws.workspaceName, ws.workspace)
const wsid = getWorkspaceId(ws.workspace)
const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid))
// const wsid = getWorkspaceId(ws.workspace)
// const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid))
// Let's connect and update account information.
await fixEmailInWorkspace(endpoint, ws, oldEmail, newEmail)
}
}
// // Let's connect and update account information.
// await fixEmailInWorkspace(endpoint, ws, oldEmail, newEmail)
// }
// }
async function fixEmailInWorkspace (
transactorUrl: string,
ws: Workspace,
oldEmail: string,
newEmail: string
): Promise<void> {
const connection = await connect(transactorUrl, { name: ws.workspace }, undefined, {
mode: 'backup',
model: 'upgrade', // Required for force all clients reload after operation will be complete.
admin: 'true'
})
try {
const personAccount = await connection.findOne(contact.class.PersonAccount, { email: oldEmail })
// async function fixEmailInWorkspace (
// transactorUrl: string,
// ws: Workspace,
// oldEmail: string,
// newEmail: string
// ): Promise<void> {
// const connection = await connect(transactorUrl, { name: ws.workspace }, undefined, {
// mode: 'backup',
// model: 'upgrade', // Required for force all clients reload after operation will be complete.
// admin: 'true'
// })
// try {
// const personAccount = await connection.findOne(contact.class.PersonAccount, { email: oldEmail })
if (personAccount !== undefined) {
console.log('update account in ', ws.workspace)
const ops = new TxOperations(connection, core.account.ConfigUser)
await ops.update(personAccount, { email: newEmail })
}
} catch (err: any) {
console.error(err)
} finally {
await connection.close()
}
}
// if (personAccount !== undefined) {
// console.log('update account in ', ws.workspace)
// const ops = new TxOperations(connection, core.account.ConfigUser)
// await ops.update(personAccount, { email: newEmail })
// }
// } catch (err: any) {
// console.error(err)
// } finally {
// await connection.close()
// }
// }
interface GithubUserResult {
login: string | null
code: number
rateLimitReset?: number | null
}
// interface GithubUserResult {
// login: string | null
// code: number
// rateLimitReset?: number | null
// }
async function getGithubUser (githubId: string, ghToken?: string): Promise<GithubUserResult> {
const options =
ghToken !== undefined
? {
headers: {
Authorization: `Bearer ${ghToken}`,
Accept: 'application/vnd.github.v3+json'
}
}
: undefined
const res = await fetch(`https://api.github.com/user/${githubId}`, options)
// async function getGithubUser (githubId: string, ghToken?: string): Promise<GithubUserResult> {
// const options =
// ghToken !== undefined
// ? {
// headers: {
// Authorization: `Bearer ${ghToken}`,
// Accept: 'application/vnd.github.v3+json'
// }
// }
// : undefined
// const res = await fetch(`https://api.github.com/user/${githubId}`, options)
if (res.status === 200) {
return {
login: (await res.json()).login,
code: 200
}
}
// if (res.status === 200) {
// return {
// login: (await res.json()).login,
// code: 200
// }
// }
if (res.status === 403) {
const rateLimitReset = res.headers.get('X-RateLimit-Reset')
return {
login: null,
code: res.status,
rateLimitReset: rateLimitReset != null ? parseInt(rateLimitReset) * 1000 : null
}
}
// if (res.status === 403) {
// const rateLimitReset = res.headers.get('X-RateLimit-Reset')
// return {
// login: null,
// code: res.status,
// rateLimitReset: rateLimitReset != null ? parseInt(rateLimitReset) * 1000 : null
// }
// }
return {
login: null,
code: res.status
}
}
// return {
// login: null,
// code: res.status
// }
// }
export async function fillGithubUsers (ctx: MeasureContext, db: AccountDB, ghToken?: string): Promise<void> {
const githubAccounts = await db.account.find({ githubId: { $ne: null } })
if (githubAccounts.length === 0) {
ctx.info('no github accounts found')
return
}
// export async function fillGithubUsers (ctx: MeasureContext, db: AccountDB, ghToken?: string): Promise<void> {
// const githubAccounts = await db.account.find({ githubId: { $ne: null } })
// if (githubAccounts.length === 0) {
// ctx.info('no github accounts found')
// return
// }
const accountsToProcess = githubAccounts.filter(({ githubId, githubUser }) => githubUser == null && githubId != null)
if (accountsToProcess.length === 0) {
ctx.info('no github accounts left to fill')
return
}
// const accountsToProcess = githubAccounts.filter(({ githubId, githubUser }) => githubUser == null && githubId != null)
// if (accountsToProcess.length === 0) {
// ctx.info('no github accounts left to fill')
// return
// }
ctx.info('processing github accounts', { total: accountsToProcess.length })
const defaultRetryTimeout = 1000 * 60 * 5 // 5 minutes
let processed = 0
for (const account of accountsToProcess) {
while (true) {
try {
if (account.githubId == null) break
let username: string | undefined
if (account.email.startsWith('github:')) {
username = account.email.slice(7)
} else {
const githubUserRes = await getGithubUser(account.githubId, ghToken)
if (githubUserRes.code === 200 && githubUserRes.login != null) {
username = githubUserRes.login
} else if (githubUserRes.code === 404) {
ctx.info('github user not found', { githubId: account.githubId })
break
} else if (githubUserRes.code === 403) {
const timeout =
githubUserRes.rateLimitReset != null
? githubUserRes.rateLimitReset - Date.now() + 1000
: defaultRetryTimeout
ctx.info('rate limit exceeded. Retrying in ', {
githubId: account.githubId,
retryTimeoutMin: Math.ceil(timeout / (1000 * 60))
})
await new Promise((resolve) => setTimeout(resolve, timeout))
} else {
ctx.error('failed to get github user', { githubId: account.githubId, ...githubUserRes })
break
}
}
if (username != null) {
await db.account.updateOne({ _id: account._id }, { githubUser: username.toLowerCase() })
ctx.info('github user added', { githubId: account.githubId, githubUser: username.toLowerCase() })
break
}
} catch (err: any) {
ctx.error('failed to fill github user', { githubId: account.githubId, err })
break
}
}
processed++
if (processed % 100 === 0) {
ctx.info('processing accounts:', { processed, of: accountsToProcess.length })
}
}
ctx.info('finished processing accounts:', { processed, of: accountsToProcess.length })
}
// ctx.info('processing github accounts', { total: accountsToProcess.length })
// const defaultRetryTimeout = 1000 * 60 * 5 // 5 minutes
// let processed = 0
// for (const account of accountsToProcess) {
// while (true) {
// try {
// if (account.githubId == null) break
// let username: string | undefined
// if (account.email.startsWith('github:')) {
// username = account.email.slice(7)
// } else {
// const githubUserRes = await getGithubUser(account.githubId, ghToken)
// if (githubUserRes.code === 200 && githubUserRes.login != null) {
// username = githubUserRes.login
// } else if (githubUserRes.code === 404) {
// ctx.info('github user not found', { githubId: account.githubId })
// break
// } else if (githubUserRes.code === 403) {
// const timeout =
// githubUserRes.rateLimitReset != null
// ? githubUserRes.rateLimitReset - Date.now() + 1000
// : defaultRetryTimeout
// ctx.info('rate limit exceeded. Retrying in ', {
// githubId: account.githubId,
// retryTimeoutMin: Math.ceil(timeout / (1000 * 60))
// })
// await new Promise((resolve) => setTimeout(resolve, timeout))
// } else {
// ctx.error('failed to get github user', { githubId: account.githubId, ...githubUserRes })
// break
// }
// }
// if (username != null) {
// await db.account.updateOne({ _id: account._id }, { githubUser: username.toLowerCase() })
// ctx.info('github user added', { githubId: account.githubId, githubUser: username.toLowerCase() })
// break
// }
// } catch (err: any) {
// ctx.error('failed to fill github user', { githubId: account.githubId, err })
// break
// }
// }
// processed++
// if (processed % 100 === 0) {
// ctx.info('processing accounts:', { processed, of: accountsToProcess.length })
// }
// }
// ctx.info('finished processing accounts:', { processed, of: accountsToProcess.length })
// }
+105 -68
View File
@@ -12,25 +12,26 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
/* eslint-disable @typescript-eslint/no-unused-vars */
import core, {
AccountRole,
MeasureMetricsContext,
RateLimiter,
TxOperations,
concatLink,
generateId,
getWorkspaceId,
metricsToString,
newMetrics,
systemAccountEmail,
type Account,
systemAccountUuid,
buildSocialIdString,
type PersonId,
type BackupClient,
type BenchmarkDoc,
type Client,
type Metrics,
type Ref,
type WorkspaceId
type WorkspaceUuid,
SocialIdType,
type PersonUuid
} from '@hcengineering/core'
import { generateToken } from '@hcengineering/server-token'
import { connect } from '@hcengineering/server-tool'
@@ -42,15 +43,15 @@ import os from 'os'
import { Worker, isMainThread, parentPort } from 'worker_threads'
import { CSVWriter } from './csv'
import { AvatarType, type PersonAccount } from '@hcengineering/contact'
import { AvatarType, getPersonBySocialId } from '@hcengineering/contact'
import contact from '@hcengineering/model-contact'
import recruit from '@hcengineering/model-recruit'
import { type Vacancy } from '@hcengineering/recruit'
import { WebSocket } from 'ws'
interface StartMessage {
email: string
workspaceId: WorkspaceId
account: PersonUuid
workspaceId: WorkspaceUuid
transactorUrl: string
id: number
idd: number
@@ -87,8 +88,8 @@ interface PendingMsg extends Msg {
}
export async function benchmark (
workspaceId: WorkspaceId[],
users: Map<string, string[]>,
workspaceId: WorkspaceUuid[],
users: Map<WorkspaceUuid, PersonUuid[]>,
accountsUrl: string,
cmd: {
from: number
@@ -177,11 +178,11 @@ export async function benchmark (
let transfer: number = 0
let oldTransfer: number = 0
const token = generateToken(systemAccountEmail, workspaceId[0])
const token = generateToken(systemAccountUuid, workspaceId[0])
setMetadata(serverClientPlugin.metadata.Endpoint, accountsUrl)
const endpoint = await getTransactorEndpoint(token, 'external')
console.log('monitor endpoint', endpoint, 'workspace', workspaceId[0].name)
console.log('monitor endpoint', endpoint, 'workspace', workspaceId[0])
const monitorConnection = isMainThread
? ((await ctx.with(
'connect',
@@ -303,13 +304,13 @@ export async function benchmark (
.map(async (it) => {
const wsid = workspaceId[randNum(workspaceId.length)]
const workId = 'w-' + i + '-' + it
const wsUsers = users.get(wsid.name) ?? []
const wsUsers = users.get(wsid) ?? []
const token = generateToken(systemAccountEmail, wsid)
const token = generateToken(systemAccountUuid, wsid)
const endpoint = await getTransactorEndpoint(token, 'external')
console.log('endpoint', endpoint, 'workspace', wsid.name)
console.log('endpoint', endpoint, 'workspace', wsid)
const msg: StartMessage = {
email: wsUsers[randNum(wsUsers.length)],
account: wsUsers[randNum(wsUsers.length)],
workspaceId: wsid,
transactorUrl: endpoint,
id: i,
@@ -371,10 +372,11 @@ export function benchmarkWorker (): void {
setMetadata(client.metadata.UseProtocolCompression, msg.compression)
console.log('connecting to', msg.workspaceId)
connection = await connect(msg.transactorUrl, msg.workspaceId, msg.email)
connection = await connect(msg.transactorUrl, msg.workspaceId, msg.account)
if (msg.options.mode === 'find-all') {
const opt = new TxOperations(connection, (core.account.System + '_benchmark') as Ref<Account>)
const benchmarkPersonId = (core.account.System + '_benchmark') as PersonId
const opt = new TxOperations(connection, benchmarkPersonId)
parentPort?.postMessage({
type: 'operate',
workId: msg.workId
@@ -479,39 +481,41 @@ export function benchmarkWorker (): void {
export type StressBenchmarkMode = 'wrong' | 'connect-disconnect'
export async function stressBenchmark (transactor: string, mode: StressBenchmarkMode): Promise<void> {
if (mode === 'wrong') {
console.log('Stress with wrong workspace/email')
let counter = 0
const rate = new RateLimiter(1)
while (true) {
try {
counter++
console.log('Attempt', counter)
const token = generateToken(generateId(), { name: generateId() })
await rate.add(async () => {
try {
const ws = new WebSocket(concatLink(transactor, token))
await new Promise<void>((resolve) => {
ws.onopen = () => {
resolve()
}
})
// ws.close()
// await createClient(transactor, token, undefined, 50)
console.log('out')
} catch (err: any) {
console.error(err)
}
})
} catch (err: any) {
// Ignore
}
}
}
// TODO: FIXME
throw new Error('Not implemented')
// if (mode === 'wrong') {
// console.log('Stress with wrong workspace/email')
// let counter = 0
// const rate = new RateLimiter(1)
// while (true) {
// try {
// counter++
// console.log('Attempt', counter)
// const token = generateToken(generateId(), generateId())
// await rate.add(async () => {
// try {
// const ws = new WebSocket(concatLink(transactor, token))
// await new Promise<void>((resolve) => {
// ws.onopen = () => {
// resolve()
// }
// })
// // ws.close()
// // await createClient(transactor, token, undefined, 50)
// console.log('out')
// } catch (err: any) {
// console.error(err)
// }
// })
// } catch (err: any) {
// // Ignore
// }
// }
// }
}
export async function testFindAll (endpoint: string, workspace: string, email: string): Promise<void> {
const connection = await connect(endpoint, getWorkspaceId(workspace), email)
export async function testFindAll (endpoint: string, workspace: WorkspaceUuid, account: PersonUuid): Promise<void> {
const connection = await connect(endpoint, workspace, account)
try {
const client = new TxOperations(connection, core.account.System)
const start = Date.now()
@@ -533,22 +537,23 @@ export async function testFindAll (endpoint: string, workspace: string, email: s
export async function generateWorkspaceData (
endpoint: string,
workspace: string,
workspace: WorkspaceUuid,
parallel: boolean,
user: string
email: string
): Promise<void> {
const connection = await connect(endpoint, getWorkspaceId(workspace))
const connection = await connect(endpoint, workspace)
const client = new TxOperations(connection, core.account.System)
try {
const acc = await client.findOne(contact.class.PersonAccount, { email: user })
if (acc == null) {
const emailSocialString = buildSocialIdString({ type: SocialIdType.EMAIL, value: email })
const person = await getPersonBySocialId(client, emailSocialString)
if (person == null) {
throw new Error('User not found')
}
const employees: Ref<PersonAccount>[] = [acc._id]
const employees: PersonId[] = [emailSocialString]
const start = Date.now()
for (let i = 0; i < 100; i++) {
const acc = await generateEmployee(client)
employees.push(acc)
const socialString = await generateEmployee(client)
employees.push(socialString)
}
if (parallel) {
const promises: Promise<void>[] = []
@@ -567,24 +572,39 @@ export async function generateWorkspaceData (
}
}
export async function generateEmployee (client: TxOperations): Promise<Ref<PersonAccount>> {
export async function generateEmployee (client: TxOperations): Promise<PersonId> {
const personUuid = generateId() as unknown as PersonUuid // TODO: will it work or need to actually be a UUID?
const personId = await client.createDoc(contact.class.Person, contact.space.Contacts, {
name: generateId().toString(),
city: '',
avatarType: AvatarType.COLOR
avatarType: AvatarType.COLOR,
personUuid
})
await client.createMixin(personId, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, {
active: true
})
const acc = await client.createDoc(contact.class.PersonAccount, core.space.Model, {
person: personId,
role: AccountRole.User,
email: personId
})
return acc
const socialString = buildSocialIdString({ type: SocialIdType.HULY, value: personUuid })
await client.addCollection(
contact.class.SocialIdentity,
contact.space.Contacts,
personId,
contact.class.Person,
'socialIds',
{
type: SocialIdType.HULY,
value: personUuid,
key: socialString,
confirmed: true
}
)
return socialString
}
async function generateVacancy (client: TxOperations, members: Ref<PersonAccount>[]): Promise<void> {
async function generateVacancy (client: TxOperations, members: PersonId[]): Promise<void> {
// generate vacancies
const _id = generateId<Vacancy>()
await client.createDoc(
@@ -602,13 +622,30 @@ async function generateVacancy (client: TxOperations, members: Ref<PersonAccount
},
_id
)
for (let i = 0; i < 100; i++) {
// generate candidate
const personUuid = generateId() as unknown as PersonUuid // TODO: will it work or need to actually be a UUID?
const personId = await client.createDoc(contact.class.Person, contact.space.Contacts, {
name: generateId().toString(),
city: '',
avatarType: AvatarType.COLOR
avatarType: AvatarType.COLOR,
personUuid
})
const socialString = buildSocialIdString({ type: SocialIdType.HULY, value: personUuid })
await client.addCollection(
contact.class.SocialIdentity,
contact.space.Contacts,
personId,
contact.class.Person,
'socialIds',
{
type: SocialIdType.HULY,
value: personUuid,
key: socialString,
confirmed: true
}
)
await client.createMixin(personId, contact.class.Person, contact.space.Contacts, recruit.mixin.Candidate, {})
// generate applicants
await client.addCollection(recruit.class.Applicant, _id, personId, recruit.mixin.Candidate, 'applications', {
+305 -307
View File
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { getAccountDB, listWorkspacesRaw } from '@hcengineering/account'
/* eslint-disable @typescript-eslint/no-unused-vars */
import { getAccountDB } from '@hcengineering/account'
import calendar from '@hcengineering/calendar'
import chunter, { type ChatMessage } from '@hcengineering/chunter'
import { loadCollabYdoc, saveCollabYdoc, yDocToBuffer } from '@hcengineering/collaboration'
@@ -52,12 +52,10 @@ import core, {
TxProcessor,
type TxRemoveDoc,
type TxUpdateDoc,
type WorkspaceId,
type WorkspaceUuid,
type WorkspaceDataId,
generateId,
getObjectValue,
getWorkspaceId,
makeDocCollabId,
systemAccountEmail,
toIdMap,
updateAttribute
} from '@hcengineering/core'
@@ -80,7 +78,7 @@ import { type Db } from 'mongodb'
export async function cleanWorkspace (
ctx: MeasureContext,
mongoUrl: string,
workspaceId: WorkspaceId,
workspaceId: WorkspaceUuid,
storageAdapter: StorageAdapter,
transactorUrl: string,
opt: { recruit: boolean, tracker: boolean, removedTx: boolean }
@@ -174,10 +172,10 @@ export async function cleanWorkspace (
export async function fixMinioBW (
ctx: MeasureContext,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
storageService: StorageAdapter
): Promise<void> {
console.log('try clean bw miniature for ', workspaceId.name)
console.log('try clean bw miniature for ', workspaceId)
const from = new Date(new Date().setDate(new Date().getDate() - 7)).getTime()
const list = await storageService.listStream(ctx, workspaceId)
let removed = 0
@@ -200,7 +198,7 @@ export async function fixMinioBW (
console.log('FINISH, removed: ', removed)
}
export async function cleanRemovedTransactions (workspaceId: WorkspaceId, transactorUrl: string): Promise<void> {
export async function cleanRemovedTransactions (workspaceId: WorkspaceUuid, transactorUrl: string): Promise<void> {
const connection = (await connect(transactorUrl, workspaceId, undefined, {
mode: 'backup'
})) as unknown as CoreClient & BackupClient
@@ -232,7 +230,7 @@ export async function cleanRemovedTransactions (workspaceId: WorkspaceId, transa
}
}
export async function optimizeModel (workspaceId: WorkspaceId, transactorUrl: string): Promise<void> {
export async function optimizeModel (workspaceId: WorkspaceUuid, transactorUrl: string): Promise<void> {
const connection = (await connect(transactorUrl, workspaceId, undefined, {
mode: 'backup',
model: 'upgrade'
@@ -303,7 +301,7 @@ export async function optimizeModel (workspaceId: WorkspaceId, transactorUrl: st
await connection.close()
}
}
export async function cleanArchivedSpaces (workspaceId: WorkspaceId, transactorUrl: string): Promise<void> {
export async function cleanArchivedSpaces (workspaceId: WorkspaceUuid, transactorUrl: string): Promise<void> {
const connection = (await connect(transactorUrl, workspaceId, undefined, {
mode: 'backup'
})) as unknown as CoreClient & BackupClient
@@ -346,7 +344,7 @@ export async function cleanArchivedSpaces (workspaceId: WorkspaceId, transactorU
}
}
export async function fixCommentDoubleIdCreate (workspaceId: WorkspaceId, transactorUrl: string): Promise<void> {
export async function fixCommentDoubleIdCreate (workspaceId: WorkspaceUuid, transactorUrl: string): Promise<void> {
const connection = (await connect(transactorUrl, workspaceId, undefined, {
mode: 'backup'
})) as unknown as CoreClient & BackupClient
@@ -398,7 +396,7 @@ export async function fixCommentDoubleIdCreate (workspaceId: WorkspaceId, transa
const DOMAIN_TAGS = 'tags' as Domain
export async function fixSkills (
mongoUrl: string,
workspaceId: WorkspaceId,
workspaceId: WorkspaceUuid,
transactorUrl: string,
step: string
): Promise<void> {
@@ -667,7 +665,7 @@ function groupBy<T extends Doc> (docs: T[], key: string): Record<any, T[]> {
export async function restoreRecruitingTaskTypes (
mongoUrl: string,
workspaceId: WorkspaceId,
workspaceId: WorkspaceUuid,
transactorUrl: string
): Promise<void> {
const connection = (await connect(transactorUrl, workspaceId, undefined, {
@@ -831,7 +829,7 @@ export async function restoreRecruitingTaskTypes (
export async function restoreHrTaskTypesFromUpdates (
mongoUrl: string,
workspaceId: WorkspaceId,
workspaceId: WorkspaceUuid,
transactorUrl: string
): Promise<void> {
const connection = (await connect(transactorUrl, workspaceId, undefined, {
@@ -1044,318 +1042,318 @@ export async function removeDuplicateIds (
accountsUrl: string,
initWorkspacesStr: string
): Promise<void> {
const state = 'REMOVE_DUPLICATE_IDS'
const [accountsDb, closeAccountsDb] = await getAccountDB(mongodbUri)
const mongoClient = getMongoClient(mongodbUri)
const _client = await mongoClient.getClient()
// disable spaces while change hardocded ids
const skippedDomains: string[] = [DOMAIN_DOC_INDEX_STATE, DOMAIN_BENCHMARK, DOMAIN_TX, DOMAIN_SPACE]
try {
const workspaces = await listWorkspacesRaw(accountsDb)
workspaces.sort((a, b) => b.lastVisit - a.lastVisit)
const initWorkspaces = initWorkspacesStr.split(';')
const initWS = workspaces.filter((p) => initWorkspaces.includes(p.workspace))
const ids = new Map<string, RelatedDocument[]>()
for (const workspace of initWS) {
const workspaceId = getWorkspaceId(workspace.workspace)
const db = getWorkspaceMongoDB(_client, workspaceId)
// TODO: FIXME
throw new Error('Not implemented')
// const state = 'REMOVE_DUPLICATE_IDS'
// const [accountsDb, closeAccountsDb] = await getAccountDB(mongodbUri)
// const mongoClient = getMongoClient(mongodbUri)
// const _client = await mongoClient.getClient()
// // disable spaces while change hardocded ids
// const skippedDomains: string[] = [DOMAIN_DOC_INDEX_STATE, DOMAIN_BENCHMARK, DOMAIN_TX, DOMAIN_SPACE]
// try {
// const workspaces = await listWorkspacesRaw(accountsDb)
// workspaces.sort((a, b) => b.status.lastVisit - a.status.lastVisit)
// const initWorkspaces = initWorkspacesStr.split(';')
// const initWS = workspaces.filter((p) => initWorkspaces.includes(p.uuid))
// const ids = new Map<string, RelatedDocument[]>()
// for (const workspace of initWS) {
// const db = getWorkspaceMongoDB(_client, workspace.dataId)
const txex = await db.collection(DOMAIN_TX).find<TxCUD<Doc>>({}).toArray()
const txesArr = []
for (const obj of txex) {
if (obj.objectSpace === core.space.Model && !isPersonAccount(obj)) {
continue
}
txesArr.push({ _id: obj._id, _class: obj._class })
}
txesArr.filter((it, idx, array) => array.findIndex((pt) => pt._id === it._id) === idx)
ids.set(DOMAIN_TX, txesArr)
// const txex = await db.collection(DOMAIN_TX).find<TxCUD<Doc>>({}).toArray()
// const txesArr = []
// for (const obj of txex) {
// if (obj.objectSpace === core.space.Model) {
// continue
// }
// txesArr.push({ _id: obj._id, _class: obj._class })
// }
// txesArr.filter((it, idx, array) => array.findIndex((pt) => pt._id === it._id) === idx)
// ids.set(DOMAIN_TX, txesArr)
const colls = await db.collections()
for (const coll of colls) {
if (skippedDomains.includes(coll.collectionName)) continue
const arr = ids.get(coll.collectionName) ?? []
const data = await coll.find<RelatedDocument>({}, { projection: { _id: 1, _class: 1 } }).toArray()
for (const obj of data) {
arr.push(obj)
}
ids.set(coll.collectionName, arr)
}
// const colls = await db.collections()
// for (const coll of colls) {
// if (skippedDomains.includes(coll.collectionName)) continue
// const arr = ids.get(coll.collectionName) ?? []
// const data = await coll.find<RelatedDocument>({}, { projection: { _id: 1, _class: 1 } }).toArray()
// for (const obj of data) {
// arr.push(obj)
// }
// ids.set(coll.collectionName, arr)
// }
const arr = ids.get(DOMAIN_MODEL) ?? []
const data = await db
.collection(DOMAIN_TX)
.find<TxCUD<Doc>>(
{ objectSpace: core.space.Model },
{ projection: { objectId: 1, objectClass: 1, modifiedBy: 1 } }
)
.toArray()
for (const obj of data) {
if (
(obj.modifiedBy === core.account.ConfigUser || obj.modifiedBy === core.account.System) &&
!isPersonAccount(obj)
) {
continue
}
if (obj.objectId === core.account.ConfigUser || obj.objectId === core.account.System) continue
arr.push({ _id: obj.objectId, _class: obj.objectClass })
}
arr.filter((it, idx, array) => array.findIndex((pt) => pt._id === it._id) === idx)
ids.set(DOMAIN_MODEL, arr)
}
// const arr = ids.get(DOMAIN_MODEL) ?? []
// const data = await db
// .collection(DOMAIN_TX)
// .find<TxCUD<Doc>>(
// { objectSpace: core.space.Model },
// { projection: { objectId: 1, objectClass: 1, modifiedBy: 1 } }
// )
// .toArray()
// for (const obj of data) {
// if (obj.modifiedBy === core.account.ConfigUser || obj.modifiedBy === core.account.System) {
// continue
// }
// if (obj.objectId === core.account.ConfigUser || obj.objectId === core.account.System) continue
// arr.push({ _id: obj.objectId, _class: obj.objectClass })
// }
// arr.filter((it, idx, array) => array.findIndex((pt) => pt._id === it._id) === idx)
// ids.set(DOMAIN_MODEL, arr)
// }
for (let index = 0; index < workspaces.length; index++) {
const workspace = workspaces[index]
// we should skip init workspace first time, for case if something went wrong
if (initWorkspaces.includes(workspace.workspace)) continue
// for (let index = 0; index < workspaces.length; index++) {
// const workspace = workspaces[index]
// // we should skip init workspace first time, for case if something went wrong
// if (initWorkspaces.includes(workspace.uuid)) continue
ctx.info(`Processing workspace ${workspace.workspaceName ?? workspace.workspace}`)
const workspaceId = getWorkspaceId(workspace.workspace)
const db = getWorkspaceMongoDB(_client, workspaceId)
const check = await db.collection(DOMAIN_MIGRATION).findOne({ state, plugin: workspace.workspace })
if (check != null) continue
// ctx.info(`Processing workspace ${workspace.name ?? workspace.url ?? workspace.uuid}`)
// const workspaceId = workspace.uuid
// const wsDataId = workspace.dataId ?? workspaceId
// const db = getWorkspaceMongoDB(_client, workspace.dataId)
// const plugins = [workspace.uuid]
// if (workspace.dataId != null) {
// plugins.push(workspace.dataId)
// }
const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, workspaceId))
const wsClient = (await connect(endpoint, workspaceId, undefined, {
model: 'upgrade'
})) as CoreClient & BackupClient
for (const set of ids) {
if (set[1].length === 0) continue
for (const doc of set[1]) {
await updateId(ctx, wsClient, db, storageAdapter, workspaceId, doc)
}
}
await wsClient.sendForceClose()
await wsClient.close()
await db.collection<MigrationState>(DOMAIN_MIGRATION).insertOne({
_id: generateId(),
state,
plugin: workspace.workspace,
space: core.space.Configuration,
modifiedOn: Date.now(),
modifiedBy: core.account.System,
_class: core.class.MigrationState
})
ctx.info(`Done ${index} / ${workspaces.length - initWorkspaces.length}`)
}
} catch (err: any) {
console.trace(err)
} finally {
mongoClient.close()
closeAccountsDb()
}
// const check = await db.collection(DOMAIN_MIGRATION).findOne({ state, plugin: { $in: plugins } })
// if (check != null) continue
// const endpoint = await getTransactorEndpoint(generateToken(systemAccountUuid, workspaceId, { service: 'tool' }))
// const wsClient = (await connect(endpoint, workspaceId, undefined, {
// model: 'upgrade'
// })) as CoreClient & BackupClient
// for (const set of ids) {
// if (set[1].length === 0) continue
// for (const doc of set[1]) {
// await updateId(ctx, wsClient, db, storageAdapter, wsDataId, doc)
// }
// }
// await wsClient.sendForceClose()
// await wsClient.close()
// await db.collection<MigrationState>(DOMAIN_MIGRATION).insertOne({
// _id: generateId(),
// state,
// plugin: workspace.uuid,
// space: core.space.Configuration,
// modifiedOn: Date.now(),
// modifiedBy: core.account.System,
// _class: core.class.MigrationState
// })
// ctx.info(`Done ${index} / ${workspaces.length - initWorkspaces.length}`)
// }
// } catch (err: any) {
// console.trace(err)
// } finally {
// mongoClient.close()
// closeAccountsDb()
// }
}
function isPersonAccount (tx: TxCUD<Doc>): boolean {
return tx.objectClass === contact.class.PersonAccount
}
// async function update<T extends Doc> (h: Hierarchy, db: Db, doc: T, update: DocumentUpdate<T>): Promise<void> {
// await db
// .collection(h.getDomain(doc._class))
// .updateOne({ _id: doc._id }, { $set: { ...update, '%hash%': Date.now().toString(16) } })
// }
async function update<T extends Doc> (h: Hierarchy, db: Db, doc: T, update: DocumentUpdate<T>): Promise<void> {
await db
.collection(h.getDomain(doc._class))
.updateOne({ _id: doc._id }, { $set: { ...update, '%hash%': Date.now().toString(16) } })
}
// async function updateId (
// ctx: MeasureContext,
// client: CoreClient & BackupClient,
// db: Db,
// storage: StorageAdapter,
// workspaceId: WorkspaceDataId,
// docRef: RelatedDocument
// ): Promise<void> {
// const h = client.getHierarchy()
// const txop = new TxOperations(client, core.account.System)
// try {
// // chech the doc exists
// const doc = await client.findOne(docRef._class, { _id: docRef._id })
// if (doc === undefined) return
// const domain = h.getDomain(doc._class)
// const newId = generateId()
async function updateId (
ctx: MeasureContext,
client: CoreClient & BackupClient,
db: Db,
storage: StorageAdapter,
workspaceId: WorkspaceId,
docRef: RelatedDocument
): Promise<void> {
const h = client.getHierarchy()
const txop = new TxOperations(client, core.account.System)
try {
// chech the doc exists
const doc = await client.findOne(docRef._class, { _id: docRef._id })
if (doc === undefined) return
const domain = h.getDomain(doc._class)
const newId = generateId()
// // update txes
// await db
// .collection(DOMAIN_TX)
// .updateMany({ objectId: doc._id }, { $set: { objectId: newId, '%hash%': Date.now().toString(16) } })
// update txes
await db
.collection(DOMAIN_TX)
.updateMany({ objectId: doc._id }, { $set: { objectId: newId, '%hash%': Date.now().toString(16) } })
// // update nested txes
// await db
// .collection(DOMAIN_TX)
// .updateMany({ 'tx.objectId': doc._id }, { $set: { 'tx.objectId': newId, '%hash%': Date.now().toString(16) } })
// update nested txes
await db
.collection(DOMAIN_TX)
.updateMany({ 'tx.objectId': doc._id }, { $set: { 'tx.objectId': newId, '%hash%': Date.now().toString(16) } })
// // we have generated ids for calendar, let's update in
// if (h.isDerived(doc._class, core.class.Account)) {
// await updateId(ctx, client, db, storage, workspaceId, {
// _id: `${doc._id}_calendar` as Ref<Doc>,
// _class: calendar.class.Calendar
// })
// }
// we have generated ids for calendar, let's update in
if (h.isDerived(doc._class, core.class.Account)) {
await updateId(ctx, client, db, storage, workspaceId, {
_id: `${doc._id}_calendar` as Ref<Doc>,
_class: calendar.class.Calendar
})
}
// // update backlinks
// const backlinks = await client.findAll(activity.class.ActivityReference, { attachedTo: doc._id })
// for (const backlink of backlinks) {
// const contentDoc = await client.findOne(backlink.attachedDocClass ?? backlink.srcDocClass, {
// _id: backlink.attachedDocId ?? backlink.srcDocClass
// })
// if (contentDoc !== undefined) {
// const attrs = h.getAllAttributes(contentDoc._class)
// for (const [attrName, attr] of attrs) {
// if (attr.type._class === core.class.TypeMarkup) {
// const markup = (contentDoc as any)[attrName] as Markup
// const newMarkup = markup.replaceAll(doc._id, newId)
// await update(h, db, contentDoc, { [attrName]: newMarkup })
// } else if (attr.type._class === core.class.TypeCollaborativeDoc) {
// const collabId = makeDocCollabId(contentDoc, attr.name)
// await updateYDoc(ctx, collabId, storage, workspaceId, contentDoc, newId, doc)
// }
// }
// }
// await update(h, db, backlink, { attachedTo: newId, message: backlink.message.replaceAll(doc._id, newId) })
// }
// update backlinks
const backlinks = await client.findAll(activity.class.ActivityReference, { attachedTo: doc._id })
for (const backlink of backlinks) {
const contentDoc = await client.findOne(backlink.attachedDocClass ?? backlink.srcDocClass, {
_id: backlink.attachedDocId ?? backlink.srcDocClass
})
if (contentDoc !== undefined) {
const attrs = h.getAllAttributes(contentDoc._class)
for (const [attrName, attr] of attrs) {
if (attr.type._class === core.class.TypeMarkup) {
const markup = (contentDoc as any)[attrName] as Markup
const newMarkup = markup.replaceAll(doc._id, newId)
await update(h, db, contentDoc, { [attrName]: newMarkup })
} else if (attr.type._class === core.class.TypeCollaborativeDoc) {
const collabId = makeDocCollabId(contentDoc, attr.name)
await updateYDoc(ctx, collabId, storage, workspaceId, contentDoc, newId, doc)
}
}
}
await update(h, db, backlink, { attachedTo: newId, message: backlink.message.replaceAll(doc._id, newId) })
}
// // blobs
// blobs
// await updateRefs(txop, newId, doc)
await updateRefs(txop, newId, doc)
// await updateArrRefs(txop, newId, doc)
await updateArrRefs(txop, newId, doc)
// // update docIndexState
// const docIndexState = await client.findOne(core.class.DocIndexState, { doc: doc._id })
// if (docIndexState !== undefined) {
// const { _id, space, modifiedBy, modifiedOn, createdBy, createdOn, _class, ...data } = docIndexState
// await txop.createDoc(docIndexState._class, docIndexState.space, {
// ...data,
// removed: false
// })
// await txop.update(docIndexState, { removed: true, needIndex: true })
// }
// update docIndexState
const docIndexState = await client.findOne(core.class.DocIndexState, { doc: doc._id })
if (docIndexState !== undefined) {
const { _id, space, modifiedBy, modifiedOn, createdBy, createdOn, _class, ...data } = docIndexState
await txop.createDoc(docIndexState._class, docIndexState.space, {
...data,
removed: false
})
await txop.update(docIndexState, { removed: true, needIndex: true })
}
// if (domain !== DOMAIN_MODEL) {
// const raw = await db.collection(domain).findOne({ _id: doc._id })
// await db.collection(domain).insertOne({
// ...raw,
// _id: newId as any,
// '%hash%': Date.now().toString(16)
// })
// await db.collection(domain).deleteOne({ _id: doc._id })
// }
// } catch (err: any) {
// console.error('Error processing', docRef._id)
// }
// }
if (domain !== DOMAIN_MODEL) {
const raw = await db.collection(domain).findOne({ _id: doc._id })
await db.collection(domain).insertOne({
...raw,
_id: newId as any,
'%hash%': Date.now().toString(16)
})
await db.collection(domain).deleteOne({ _id: doc._id })
}
} catch (err: any) {
console.error('Error processing', docRef._id)
}
}
// async function updateYDoc (
// ctx: MeasureContext,
// _id: CollaborativeDoc,
// storage: StorageAdapter,
// workspaceId: WorkspaceDataId,
// contentDoc: Doc,
// newId: Ref<Doc>,
// doc: RelatedDocument
// ): Promise<void> {
// try {
// const ydoc = await loadCollabYdoc(ctx, storage, workspaceId, _id)
// if (ydoc === undefined) {
// ctx.error('document content not found', { document: contentDoc._id })
// return
// }
// const buffer = yDocToBuffer(ydoc)
async function updateYDoc (
ctx: MeasureContext,
_id: CollaborativeDoc,
storage: StorageAdapter,
workspaceId: WorkspaceId,
contentDoc: Doc,
newId: Ref<Doc>,
doc: RelatedDocument
): Promise<void> {
try {
const ydoc = await loadCollabYdoc(ctx, storage, workspaceId, _id)
if (ydoc === undefined) {
ctx.error('document content not found', { document: contentDoc._id })
return
}
const buffer = yDocToBuffer(ydoc)
// const updatedYDoc = updateYDocContent(buffer, (body: Record<string, any>) => {
// const str = JSON.stringify(body)
// const updated = str.replaceAll(doc._id, newId)
// return JSON.parse(updated)
// })
const updatedYDoc = updateYDocContent(buffer, (body: Record<string, any>) => {
const str = JSON.stringify(body)
const updated = str.replaceAll(doc._id, newId)
return JSON.parse(updated)
})
// if (updatedYDoc !== undefined) {
// await saveCollabYdoc(ctx, storage, workspaceId, _id, updatedYDoc)
// }
// } catch {
// // do nothing, the collaborative doc does not sem to exist yet
// }
// }
if (updatedYDoc !== undefined) {
await saveCollabYdoc(ctx, storage, workspaceId, _id, updatedYDoc)
}
} catch {
// do nothing, the collaborative doc does not sem to exist yet
}
}
// async function updateRefs (client: TxOperations, newId: Ref<Doc>, doc: RelatedDocument): Promise<void> {
// const h = client.getHierarchy()
// const ancestors = h.getAncestors(doc._class)
// const reftos = (await client.findAll(core.class.Attribute, { 'type._class': core.class.RefTo })).filter((it) => {
// const to = it.type as RefTo<Doc>
// return ancestors.includes(h.getBaseClass(to.to))
// })
// for (const attr of reftos) {
// if (attr.name === '_id') {
// continue
// }
// const descendants = h.getDescendants(attr.attributeOf)
// for (const d of descendants) {
// if (h.isDerived(d, core.class.BenchmarkDoc)) {
// continue
// }
// if (h.isDerived(d, core.class.Tx)) {
// continue
// }
// if (h.findDomain(d) !== undefined) {
// while (true) {
// const values = await client.findAll(d, { [attr.name]: doc._id }, { limit: 100 })
// if (values.length === 0) {
// break
// }
async function updateRefs (client: TxOperations, newId: Ref<Doc>, doc: RelatedDocument): Promise<void> {
const h = client.getHierarchy()
const ancestors = h.getAncestors(doc._class)
const reftos = (await client.findAll(core.class.Attribute, { 'type._class': core.class.RefTo })).filter((it) => {
const to = it.type as RefTo<Doc>
return ancestors.includes(h.getBaseClass(to.to))
})
for (const attr of reftos) {
if (attr.name === '_id') {
continue
}
const descendants = h.getDescendants(attr.attributeOf)
for (const d of descendants) {
if (h.isDerived(d, core.class.BenchmarkDoc)) {
continue
}
if (h.isDerived(d, core.class.Tx)) {
continue
}
if (h.findDomain(d) !== undefined) {
while (true) {
const values = await client.findAll(d, { [attr.name]: doc._id }, { limit: 100 })
if (values.length === 0) {
break
}
// const builder = client.apply(doc._id)
// for (const v of values) {
// await updateAttribute(builder, v, d, { key: attr.name, attr }, newId, true)
// }
// const modelTxes = builder.txes.filter((p) => p.objectSpace === core.space.Model)
// builder.txes = builder.txes.filter((p) => p.objectSpace !== core.space.Model)
// for (const modelTx of modelTxes) {
// await client.tx(modelTx)
// }
// await builder.commit()
// }
// }
// }
// }
// }
const builder = client.apply(doc._id)
for (const v of values) {
await updateAttribute(builder, v, d, { key: attr.name, attr }, newId, true)
}
const modelTxes = builder.txes.filter((p) => p.objectSpace === core.space.Model)
builder.txes = builder.txes.filter((p) => p.objectSpace !== core.space.Model)
for (const modelTx of modelTxes) {
await client.tx(modelTx)
}
await builder.commit()
}
}
}
}
}
async function updateArrRefs (client: TxOperations, newId: Ref<Doc>, doc: RelatedDocument): Promise<void> {
const h = client.getHierarchy()
const ancestors = h.getAncestors(doc._class)
const arrs = await client.findAll(core.class.Attribute, { 'type._class': core.class.ArrOf })
for (const attr of arrs) {
if (attr.name === '_id') {
continue
}
const to = attr.type as ArrOf<Doc>
if (to.of._class !== core.class.RefTo) continue
const refto = to.of as RefTo<Doc>
if (ancestors.includes(h.getBaseClass(refto.to))) {
const descendants = h.getDescendants(attr.attributeOf)
for (const d of descendants) {
if (h.isDerived(d, core.class.BenchmarkDoc)) {
continue
}
if (h.isDerived(d, core.class.Tx)) {
continue
}
if (h.findDomain(d) !== undefined) {
while (true) {
const values = await client.findAll(attr.attributeOf, { [attr.name]: doc._id }, { limit: 100 })
if (values.length === 0) {
break
}
const builder = client.apply(doc._id)
for (const v of values) {
await updateAttribute(builder, v, d, { key: attr.name, attr }, newId, true)
}
const modelTxes = builder.txes.filter((p) => p.objectSpace === core.space.Model)
builder.txes = builder.txes.filter((p) => p.objectSpace !== core.space.Model)
for (const modelTx of modelTxes) {
await client.tx(modelTx)
}
await builder.commit()
}
}
}
}
}
}
// async function updateArrRefs (client: TxOperations, newId: Ref<Doc>, doc: RelatedDocument): Promise<void> {
// const h = client.getHierarchy()
// const ancestors = h.getAncestors(doc._class)
// const arrs = await client.findAll(core.class.Attribute, { 'type._class': core.class.ArrOf })
// for (const attr of arrs) {
// if (attr.name === '_id') {
// continue
// }
// const to = attr.type as ArrOf<Doc>
// if (to.of._class !== core.class.RefTo) continue
// const refto = to.of as RefTo<Doc>
// if (ancestors.includes(h.getBaseClass(refto.to))) {
// const descendants = h.getDescendants(attr.attributeOf)
// for (const d of descendants) {
// if (h.isDerived(d, core.class.BenchmarkDoc)) {
// continue
// }
// if (h.isDerived(d, core.class.Tx)) {
// continue
// }
// if (h.findDomain(d) !== undefined) {
// while (true) {
// const values = await client.findAll(attr.attributeOf, { [attr.name]: doc._id }, { limit: 100 })
// if (values.length === 0) {
// break
// }
// const builder = client.apply(doc._id)
// for (const v of values) {
// await updateAttribute(builder, v, d, { key: attr.name, attr }, newId, true)
// }
// const modelTxes = builder.txes.filter((p) => p.objectSpace === core.space.Model)
// builder.txes = builder.txes.filter((p) => p.objectSpace !== core.space.Model)
// for (const modelTx of modelTxes) {
// await client.tx(modelTx)
// }
// await builder.commit()
// }
// }
// }
// }
// }
// }
+2 -2
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import core, { type BackupClient, type Client as CoreClient, TxFactory, type WorkspaceId } from '@hcengineering/core'
import core, { type BackupClient, type Client as CoreClient, TxFactory, type WorkspaceUuid } from '@hcengineering/core'
import { connect } from '@hcengineering/server-tool'
function toLen (val: string, sep: string, len: number): string {
@@ -23,7 +23,7 @@ function toLen (val: string, sep: string, len: number): string {
return val
}
export async function changeConfiguration (
workspaceId: WorkspaceId,
workspaceId: WorkspaceUuid,
transactorUrl: string,
cmd: { enable?: string, disable?: string, list?: boolean }
): Promise<void> {
+117 -123
View File
@@ -1,21 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { type AccountDB, type Workspace, getAccount, getWorkspaceById } from '@hcengineering/account'
import {
type AccountDB,
listAccounts,
listWorkspacesPure,
listInvites,
updateWorkspace,
type Workspace,
type ObjectId,
getAccount,
getWorkspaceById
} from '@hcengineering/account'
import {
systemAccountUuid,
type BackupClient,
type Client,
getWorkspaceId,
type Doc,
MeasureMetricsContext,
systemAccountEmail,
type Doc
type WorkspaceUuid
} from '@hcengineering/core'
import { getMongoClient, getWorkspaceMongoDB } from '@hcengineering/mongo'
import {
@@ -55,7 +46,7 @@ export async function moveFromMongoToPG (
await moveWorkspace(accountDb, mongo, pgClient, ws, region)
console.log('Move workspace', index, workspaces.length)
} catch (err) {
console.log('Error when move workspace', ws.workspaceName ?? ws.workspace, err)
console.log('Error when move workspace', ws.name ?? ws.url, err)
throw err
}
}
@@ -73,9 +64,10 @@ async function moveWorkspace (
force = false
): Promise<void> {
try {
console.log('move workspace', ws.workspaceName ?? ws.workspace)
const wsId = getWorkspaceId(ws.workspace)
const mongoDB = getWorkspaceMongoDB(mongo, wsId)
console.log('move workspace', ws.name ?? ws.url)
const wsId = ws.uuid
// TODO: get workspace mongoDB
const mongoDB = getWorkspaceMongoDB(mongo, ws.dataId ?? wsId)
const collections = await mongoDB.collections()
let tables = collections.map((c) => c.collectionName)
if (include !== undefined) {
@@ -83,7 +75,7 @@ async function moveWorkspace (
}
await createTables(new MeasureMetricsContext('', {}), pgClient, '', tables)
const token = generateToken(systemAccountEmail, wsId)
const token = generateToken(systemAccountUuid, wsId, { service: 'tool' })
const endpoint = await getTransactorEndpoint(token, 'external')
const connection = (await connect(endpoint, wsId, undefined, {
model: 'upgrade'
@@ -94,8 +86,7 @@ async function moveWorkspace (
continue
}
const cursor = collection.find()
const current =
await pgClient`SELECT _id FROM ${pgClient(domain)} WHERE "workspaceId" = ${ws.uuid ?? ws.workspace}`
const current = await pgClient`SELECT _id FROM ${pgClient(domain)} WHERE "workspaceId" = ${ws.uuid}`
const currentIds = new Set(current.map((r) => r._id))
console.log('move domain', domain)
const docs: Doc[] = []
@@ -123,7 +114,7 @@ async function moveWorkspace (
const part = toRemove.splice(0, 100)
await retryTxn(pgClient, async (client) => {
await client.unsafe(
`DELETE FROM ${translateDomain(domain)} WHERE "workspaceId" = '${ws.workspace}' AND _id IN (${part.map((c) => `'${c}'`).join(', ')})`
`DELETE FROM ${translateDomain(domain)} WHERE "workspaceId" = '${ws.uuid}' AND _id IN (${part.map((c) => `'${c}'`).join(', ')})`
)
})
}
@@ -133,7 +124,7 @@ async function moveWorkspace (
const values: DBDoc[] = []
for (let i = 0; i < part.length; i++) {
const doc = part[i]
const d = convertDoc(domain, doc, ws.workspace)
const d = convertDoc(domain, doc, wsId)
values.push(d)
}
try {
@@ -146,11 +137,12 @@ async function moveWorkspace (
}
}
}
await updateWorkspace(accountDb, ws, { region })
// TODO: FIXME
// await updateWorkspace(accountDb, ws, { region })
await connection.sendForceClose()
await connection.close()
} catch (err) {
console.log('Error when move workspace', ws.workspaceName ?? ws.workspace, err)
console.log('Error when move workspace', ws.name ?? ws.url, err)
throw err
}
}
@@ -182,100 +174,102 @@ export async function moveAccountDbFromMongoToPG (
mongoDb: AccountDB,
pgDb: AccountDB
): Promise<void> {
// TODO: FIXME
throw new Error('Not implemented')
// [accountId, workspaceId]
const workspaceAssignments: [ObjectId, ObjectId][] = []
const accounts = await listAccounts(mongoDb)
const workspaces = await listWorkspacesPure(mongoDb)
const invites = await listInvites(mongoDb)
// const workspaceAssignments: [string, WorkspaceUuid][] = []
// const accounts = await listAccounts(mongoDb)
// const workspaces = await listWorkspacesPure(mongoDb)
// const invites = await listInvites(mongoDb)
for (const mongoAccount of accounts) {
const pgAccount = {
...mongoAccount,
_id: mongoAccount._id.toString()
}
// for (const mongoAccount of accounts) {
// const pgAccount = {
// ...mongoAccount,
// _id: mongoAccount._id.toString()
// }
delete (pgAccount as any).workspaces
// delete (pgAccount as any).workspaces
if (pgAccount.createdOn == null) {
pgAccount.createdOn = Date.now()
}
// if (pgAccount.createdOn == null) {
// pgAccount.createdOn = Date.now()
// }
if (pgAccount.first == null) {
pgAccount.first = 'NotSet'
}
// if (pgAccount.first == null) {
// pgAccount.first = 'NotSet'
// }
if (pgAccount.last == null) {
pgAccount.last = 'NotSet'
}
// if (pgAccount.last == null) {
// pgAccount.last = 'NotSet'
// }
for (const workspaceString of new Set(mongoAccount.workspaces.map((w) => w.toString()))) {
workspaceAssignments.push([pgAccount._id, workspaceString])
}
// for (const workspaceString of new Set(mongoAccount.workspaces.map((w) => w.toString()))) {
// workspaceAssignments.push([pgAccount._id, workspaceString])
// }
const exists = await getAccount(pgDb, pgAccount.email)
if (exists === null) {
await pgDb.account.insertOne(pgAccount)
ctx.info('Moved account', { email: pgAccount.email })
}
}
// const exists = await getAccount(pgDb, pgAccount.email)
// if (exists === null) {
// await pgDb.account.insertOne(pgAccount)
// ctx.info('Moved account', { email: pgAccount.email })
// }
// }
for (const mongoWorkspace of workspaces) {
const pgWorkspace = {
...mongoWorkspace,
_id: mongoWorkspace._id.toString()
}
// for (const mongoWorkspace of workspaces) {
// const pgWorkspace = {
// ...mongoWorkspace,
// _id: mongoWorkspace._id.toString()
// }
if (pgWorkspace.createdOn == null) {
pgWorkspace.createdOn = Date.now()
}
// if (pgWorkspace.createdOn == null) {
// pgWorkspace.createdOn = Date.now()
// }
// delete deprecated fields
delete (pgWorkspace as any).createProgress
delete (pgWorkspace as any).creating
delete (pgWorkspace as any).productId
delete (pgWorkspace as any).organisation
// // delete deprecated fields
// delete (pgWorkspace as any).createProgress
// delete (pgWorkspace as any).creating
// delete (pgWorkspace as any).productId
// delete (pgWorkspace as any).organisation
// assigned separately
delete (pgWorkspace as any).accounts
// // assigned separately
// delete (pgWorkspace as any).accounts
const exists = await getWorkspaceById(pgDb, pgWorkspace.workspace)
if (exists === null) {
await pgDb.workspace.insertOne(pgWorkspace)
ctx.info('Moved workspace', {
workspace: pgWorkspace.workspace,
workspaceName: pgWorkspace.workspaceName,
workspaceUrl: pgWorkspace.workspaceUrl
})
}
}
// const exists = await getWorkspaceById(pgDb, pgWorkspace.workspace)
// if (exists === null) {
// await pgDb.workspace.insertOne(pgWorkspace)
// ctx.info('Moved workspace', {
// workspace: pgWorkspace.workspace,
// workspaceName: pgWorkspace.workspaceName,
// workspaceUrl: pgWorkspace.workspaceUrl
// })
// }
// }
for (const mongoInvite of invites) {
const pgInvite = {
...mongoInvite,
_id: mongoInvite._id.toString()
}
// for (const mongoInvite of invites) {
// const pgInvite = {
// ...mongoInvite,
// _id: mongoInvite._id.toString()
// }
const exists = await pgDb.invite.findOne({ _id: pgInvite._id })
if (exists === null) {
await pgDb.invite.insertOne(pgInvite)
}
}
// const exists = await pgDb.invite.findOne({ _id: pgInvite._id })
// if (exists === null) {
// await pgDb.invite.insertOne(pgInvite)
// }
// }
const pgAssignments = (await listAccounts(pgDb)).reduce<Record<ObjectId, ObjectId[]>>((assignments, acc) => {
assignments[acc._id] = acc.workspaces
// const pgAssignments = (await listAccounts(pgDb)).reduce<Record<ObjectId, ObjectId[]>>((assignments, acc) => {
// assignments[acc._id] = acc.workspaces
return assignments
}, {})
const assignmentsToInsert = workspaceAssignments.filter(
([accountId, workspaceId]) =>
pgAssignments[accountId] === undefined || !pgAssignments[accountId].includes(workspaceId)
)
// return assignments
// }, {})
// const assignmentsToInsert = workspaceAssignments.filter(
// ([accountId, workspaceId]) =>
// pgAssignments[accountId] === undefined || !pgAssignments[accountId].includes(workspaceId)
// )
for (const [accountId, workspaceId] of assignmentsToInsert) {
await pgDb.assignWorkspace(accountId, workspaceId)
}
// for (const [accountId, workspaceId] of assignmentsToInsert) {
// await pgDb.assignWorkspace(accountId, workspaceId)
// }
ctx.info('Assignments made', { count: assignmentsToInsert.length })
// ctx.info('Assignments made', { count: assignmentsToInsert.length })
}
export async function generateUuidMissingWorkspaces (
@@ -283,18 +277,20 @@ export async function generateUuidMissingWorkspaces (
db: AccountDB,
dryRun = false
): Promise<void> {
const workspaces = await listWorkspacesPure(db)
let updated = 0
for (const ws of workspaces) {
if (ws.uuid !== undefined) continue
// TODO: FIXME
throw new Error('Not implemented')
// const workspaces = await listWorkspacesPure(db)
// let updated = 0
// for (const ws of workspaces) {
// if (ws.uuid !== undefined) continue
const uuid = new UUID().toJSON()
if (!dryRun) {
await db.workspace.updateOne({ _id: ws._id }, { uuid })
}
updated++
}
ctx.info('Assigned uuids to workspaces', { updated, total: workspaces.length })
// const uuid = new UUID().toJSON()
// if (!dryRun) {
// await db.workspace.updateOne({ _id: ws._id }, { uuid })
// }
// updated++
// }
// ctx.info('Assigned uuids to workspaces', { updated, total: workspaces.length })
}
export async function updateDataWorkspaceIdToUuid (
@@ -314,12 +310,12 @@ export async function updateDataWorkspaceIdToUuid (
// Generate uuids for all workspaces or verify they exist
await generateUuidMissingWorkspaces(ctx, accountDb, dryRun)
const workspaces = await listWorkspacesPure(accountDb)
const noUuidWss = workspaces.filter((ws) => ws.uuid === undefined)
if (noUuidWss.length > 0) {
ctx.error('Workspace uuid is required but not defined', { workspaces: noUuidWss.map((it) => it.workspace) })
throw new Error('workspace uuid is required but not defined')
}
const workspaces: Workspace[] = [] // TODO: FIXME await listWorkspacesPure(accountDb)
// const noUuidWss = workspaces.filter((ws) => ws.uuid === undefined)
// if (noUuidWss.length > 0) {
// ctx.error('Workspace uuid is required but not defined', { workspaces: noUuidWss.map((it) => it.workspace) })
// throw new Error('workspace uuid is required but not defined')
// }
const res = await pgClient`select t.table_name from information_schema.columns as c
join information_schema.tables as t on
@@ -342,13 +338,11 @@ export async function updateDataWorkspaceIdToUuid (
await retryTxn(pgClient, async (client) => {
for (const ws of workspaces) {
const uuid = ws.uuid
if (uuid === undefined) {
ctx.error('Workspace uuid is required but not defined', { workspace: ws.workspace })
throw new Error('workspace uuid is required but not defined')
}
if (ws.dataId === undefined) continue
await client`UPDATE ${client(table)} SET "workspaceId" = ${uuid} WHERE "workspaceIdOld" = ${ws.workspace} OR "workspaceIdOld" = ${uuid}`
const uuid = ws.uuid
await client`UPDATE ${client(table)} SET "workspaceId" = ${uuid} WHERE "workspaceIdOld" = ${ws.dataId} OR "workspaceIdOld" = ${uuid}`
}
})
+6 -6
View File
@@ -14,20 +14,20 @@
//
import { Client as ElasticClient } from '@elastic/elasticsearch'
import core, { DOMAIN_DOC_INDEX_STATE, toWorkspaceString, type WorkspaceId } from '@hcengineering/core'
import core, { DOMAIN_DOC_INDEX_STATE } from '@hcengineering/core'
import { getMongoClient, getWorkspaceMongoDB } from '@hcengineering/mongo'
import { type StorageAdapter } from '@hcengineering/server-core'
export async function rebuildElastic (
mongoUrl: string,
workspaceId: WorkspaceId,
dataId: string,
storageAdapter: StorageAdapter,
elasticUrl: string
): Promise<void> {
const client = getMongoClient(mongoUrl)
try {
const _client = await client.getClient()
const db = getWorkspaceMongoDB(_client, workspaceId)
const db = getWorkspaceMongoDB(_client, dataId)
await db
.collection(DOMAIN_DOC_INDEX_STATE)
.updateMany({ _class: core.class.DocIndexState }, { $set: { elastic: false } })
@@ -35,15 +35,15 @@ export async function rebuildElastic (
client.close()
}
await dropElastic(elasticUrl, workspaceId)
await dropElastic(elasticUrl, dataId)
}
async function dropElastic (elasticUrl: string, workspaceId: WorkspaceId): Promise<void> {
async function dropElastic (elasticUrl: string, dataId: string): Promise<void> {
console.log('drop existing elastic docment')
const client = new ElasticClient({
node: elasticUrl
})
const productWs = toWorkspaceString(workspaceId)
const productWs = dataId
await new Promise((resolve, reject) => {
client.indices.exists(
{
+1851 -1738
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -28,7 +28,7 @@ import core, {
type Ref,
type TxCreateDoc,
type TxUpdateDoc,
type WorkspaceId,
type WorkspaceDataId,
DOMAIN_TX,
SortingOrder,
makeCollabId,
@@ -49,7 +49,7 @@ export interface RestoreWikiContentParams {
export async function restoreWikiContentMongo (
ctx: MeasureContext,
db: Db,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
storageAdapter: StorageAdapter,
params: RestoreWikiContentParams
): Promise<void> {
@@ -119,7 +119,7 @@ export async function restoreWikiContentMongo (
export async function findWikiDocYdocName (
ctx: MeasureContext,
db: Db,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
doc: Ref<Document>
): Promise<Ref<Blob> | undefined> {
const updateContentTx = await db.collection<TxUpdateDoc<Document & { content: string }>>(DOMAIN_TX).findOne(
@@ -198,7 +198,7 @@ export interface RestoreControlledDocContentParams {
export async function restoreControlledDocContentMongo (
ctx: MeasureContext,
db: Db,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
storageAdapter: StorageAdapter,
params: RestoreWikiContentParams
): Promise<void> {
@@ -247,7 +247,7 @@ export async function restoreControlledDocContentMongo (
export async function restoreControlledDocContentForDoc (
ctx: MeasureContext,
db: Db,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
storageAdapter: StorageAdapter,
params: RestoreWikiContentParams,
doc: Doc,
@@ -302,7 +302,7 @@ export async function restoreControlledDocContentForDoc (
export async function restoreMarkupRefsMongo (
ctx: MeasureContext,
db: Db,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
hierarchy: Hierarchy,
storageAdapter: StorageAdapter
): Promise<void> {
+5 -4
View File
@@ -25,7 +25,7 @@ import core, {
type Obj,
type Ref,
SortingOrder,
type WorkspaceId
type WorkspaceUuid
} from '@hcengineering/core'
import { getMongoClient, getWorkspaceMongoDB } from '@hcengineering/mongo'
import { connect } from '@hcengineering/server-tool'
@@ -44,7 +44,7 @@ interface ObjectPropertyInfo {
}
export async function showMixinForeignAttributes (
workspaceId: WorkspaceId,
workspaceId: WorkspaceUuid,
transactorUrl: string,
cmd: { detail: boolean, mixin: string, property: string }
): Promise<void> {
@@ -81,7 +81,8 @@ export async function showMixinForeignAttributes (
export async function fixMixinForeignAttributes (
mongoUrl: string,
workspaceId: WorkspaceId,
workspaceId: WorkspaceUuid,
dataId: string | undefined,
transactorUrl: string,
cmd: { mixin: string, property: string }
): Promise<void> {
@@ -102,7 +103,7 @@ export async function fixMixinForeignAttributes (
const client = getMongoClient(mongoUrl)
try {
const _client = await client.getClient()
const db = getWorkspaceMongoDB(_client, workspaceId)
const db = getWorkspaceMongoDB(_client, dataId ?? workspaceId)
for (const [mixin, objects] of result) {
console.log('fixing', mixin)
+7 -3
View File
@@ -13,16 +13,20 @@
// limitations under the License.
//
import { type MeasureContext, type Ref, type WorkspaceId } from '@hcengineering/core'
import { type WorkspaceUuid, type MeasureContext, type Ref } from '@hcengineering/core'
import documents, { type DocumentMeta, type ProjectMeta } from '@hcengineering/controlled-documents'
import { DOMAIN_DOCUMENTS } from '@hcengineering/model-controlled-documents'
import { type Db } from 'mongodb'
import { makeRank } from '@hcengineering/task'
export async function addControlledDocumentRank (ctx: MeasureContext, db: Db, workspaceId: WorkspaceId): Promise<void> {
export async function addControlledDocumentRank (
ctx: MeasureContext,
db: Db,
workspaceId: WorkspaceUuid
): Promise<void> {
const collections = await db.listCollections().toArray()
if (collections.find((it) => it.name === DOMAIN_DOCUMENTS) === undefined) {
ctx.error('skipping migration, no collection found', { workspace: workspaceId.name })
ctx.error('skipping migration, no collection found', { workspace: workspaceId })
return
}
+9 -9
View File
@@ -18,9 +18,9 @@ import {
type Blob,
type MeasureContext,
type Ref,
type WorkspaceId,
concatLink,
RateLimiter
RateLimiter,
type WorkspaceDataId
} from '@hcengineering/core'
import { type DatalakeClient } from '@hcengineering/datalake'
import { type UploadObjectParams } from '@hcengineering/datalake/types/client'
@@ -42,7 +42,7 @@ export interface MoveFilesParams {
export async function moveFiles (
ctx: MeasureContext,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
exAdapter: StorageAdapterEx,
params: MoveFilesParams
): Promise<void> {
@@ -67,7 +67,7 @@ export async function moveFiles (
export async function showLostFiles (
ctx: MeasureContext,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
db: Db,
storageAdapter: StorageAdapter,
{ showAll }: { showAll: boolean }
@@ -95,7 +95,7 @@ async function processAdapter (
exAdapter: StorageAdapterEx,
source: StorageAdapter,
target: StorageAdapter,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
params: MoveFilesParams
): Promise<void> {
if (source === target) {
@@ -222,7 +222,7 @@ async function processFile (
ctx: MeasureContext,
source: Pick<StorageAdapter, 'get'>,
target: Pick<StorageAdapter, 'put'>,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
blob: Blob
): Promise<UploadedObjectInfo> {
const readable = await source.get(ctx, workspaceId, blob._id)
@@ -267,7 +267,7 @@ export interface CopyDatalakeParams {
export async function copyToDatalake (
ctx: MeasureContext,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
config: S3Config,
adapter: S3Service,
datalake: DatalakeClient,
@@ -374,7 +374,7 @@ export async function copyToDatalake (
export async function copyBlobToDatalake (
ctx: MeasureContext,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
blob: ListBlobResult,
config: S3Config,
adapter: S3Service,
@@ -417,7 +417,7 @@ export async function copyBlobToDatalake (
function uploadMultipart (
ctx: MeasureContext,
datalake: DatalakeClient,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
objectName: string,
stream: Readable,
metadata: UploadObjectParams
+2 -2
View File
@@ -14,7 +14,7 @@
// limitations under the License.
//
import { DOMAIN_TX, type MeasureContext, type Ref, type WorkspaceId } from '@hcengineering/core'
import { DOMAIN_TX, type WorkspaceDataId, type MeasureContext, type Ref } from '@hcengineering/core'
import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment'
import contact, { DOMAIN_CHANNEL } from '@hcengineering/model-contact'
import { DOMAIN_TELEGRAM } from '@hcengineering/model-telegram'
@@ -31,7 +31,7 @@ const LastMessages = 'last-msgs'
export async function clearTelegramHistory (
ctx: MeasureContext,
mongoUrl: string,
workspaceId: WorkspaceId,
workspaceId: WorkspaceDataId,
tgDb: string,
storageAdapter: StorageAdapter
): Promise<void> {
+29
View File
@@ -1,4 +1,7 @@
import { getWorkspaceById, getWorkspaceByUrl, type AccountDB, type Workspace } from '@hcengineering/account'
import {
systemAccountUuid,
type WorkspaceUuid,
type AttachedData,
type AttachedDoc,
type Class,
@@ -8,6 +11,8 @@ import {
type Space,
type TxOperations
} from '@hcengineering/core'
import { getTransactorEndpoint } from '@hcengineering/server-client'
import { generateToken } from '@hcengineering/server-token'
export async function findOrUpdateAttached<T extends AttachedDoc> (
client: TxOperations,
@@ -42,3 +47,27 @@ export async function findOrUpdateAttached<T extends AttachedDoc> (
}
return existingObj
}
export async function getWorkspace (db: AccountDB, workspace: string): Promise<Workspace | null> {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
let wsObj: Workspace | null
if (uuidRegex.test(workspace)) {
wsObj = await getWorkspaceById(db, workspace as WorkspaceUuid)
} else {
wsObj = await getWorkspaceByUrl(db, workspace)
}
return wsObj
}
export function getToolToken (workspace?: WorkspaceUuid): string {
return generateToken(systemAccountUuid, workspace, { service: 'tool' })
}
export async function getWorkspaceTransactorEndpoint (
workspace: WorkspaceUuid,
type: 'external' | 'internal' = 'external'
): Promise<string> {
return await getTransactorEndpoint(getToolToken(workspace), type)
}
+6 -7
View File
@@ -14,7 +14,6 @@
// limitations under the License.
//
import contact from '@hcengineering/contact'
import core, {
type BackupClient,
type Class,
@@ -23,17 +22,17 @@ import core, {
DOMAIN_TX,
type Ref,
type Tx,
type WorkspaceId
type WorkspaceUuid
} from '@hcengineering/core'
import { getMongoClient, getWorkspaceMongoDB } from '@hcengineering/mongo'
import { connect } from '@hcengineering/server-tool'
import { generateModelDiff, printDiff } from './mdiff'
export async function diffWorkspace (mongoUrl: string, workspace: WorkspaceId, rawTxes: Tx[]): Promise<void> {
export async function diffWorkspace (mongoUrl: string, dbName: string, rawTxes: Tx[]): Promise<void> {
const client = getMongoClient(mongoUrl)
try {
const _client = await client.getClient()
const db = getWorkspaceMongoDB(_client, workspace)
const db = getWorkspaceMongoDB(_client, dbName)
console.log('diffing transactions...')
@@ -42,7 +41,7 @@ export async function diffWorkspace (mongoUrl: string, workspace: WorkspaceId, r
.find<Tx>({
objectSpace: core.space.Model,
modifiedBy: core.account.System,
objectClass: { $ne: contact.class.PersonAccount }
objectClass: { $ne: 'contact:class:PersonAccount' } // Note: we may keep these transactions in old workspaces for history purposes
})
.toArray()
@@ -50,7 +49,7 @@ export async function diffWorkspace (mongoUrl: string, workspace: WorkspaceId, r
return (
tx.objectSpace === core.space.Model &&
tx.modifiedBy === core.account.System &&
(tx as any).objectClass !== contact.class.PersonAccount
(tx as any).objectClass !== 'contact:class:PersonAccount'
)
})
@@ -72,7 +71,7 @@ export async function diffWorkspace (mongoUrl: string, workspace: WorkspaceId, r
}
export async function updateField (
workspaceId: WorkspaceId,
workspaceId: WorkspaceUuid,
transactorUrl: string,
cmd: { objectId: string, objectClass: string, type: string, attribute: string, value: string, domain: string }
): Promise<void> {
+4 -3
View File
@@ -38,7 +38,7 @@ import contact, { type Person } from '@hcengineering/contact'
import core, {
DOMAIN_MODEL,
IndexKind,
type Account,
type PersonId,
type Class,
type Doc,
type DocumentQuery,
@@ -62,6 +62,7 @@ import {
TypeRef,
TypeString,
TypeTimestamp,
TypePersonId,
UX,
type Builder
} from '@hcengineering/model'
@@ -226,8 +227,8 @@ export class TReaction extends TAttachedDoc implements Reaction {
@Prop(TypeString(), activity.string.Emoji)
emoji!: string
@Prop(TypeRef(core.class.Account), view.string.Created)
createBy!: Ref<Account>
@Prop(TypePersonId(), view.string.Created)
createBy!: PersonId
}
@Model(activity.class.SavedMessage, preference.class.Preference)
+59 -1
View File
@@ -15,7 +15,15 @@
import { type ActivityMessage, type DocUpdateMessage, type Reaction } from '@hcengineering/activity'
import contact from '@hcengineering/contact'
import core, { type Class, type Doc, type Domain, groupByArray, type Ref, type Space } from '@hcengineering/core'
import core, {
type Class,
type Doc,
type Domain,
groupByArray,
MeasureMetricsContext,
type Ref,
type Space
} from '@hcengineering/core'
import {
type MigrateOperation,
type MigrateUpdate,
@@ -26,6 +34,8 @@ import {
tryMigrate
} from '@hcengineering/model'
import { htmlToMarkup } from '@hcengineering/text'
import { getSocialIdByOldAccount } from '@hcengineering/model-core'
import { activityId, DOMAIN_ACTIVITY, DOMAIN_REACTION, DOMAIN_USER_MENTION } from './index'
import activity from './plugin'
@@ -181,6 +191,50 @@ async function migrateActivityMarkup (client: MigrationClient): Promise<void> {
)
}
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('activity migrateAccountsToSocialIds', {})
const socialIdByAccount = await getSocialIdByOldAccount(client)
ctx.info('processing activity reactions ', {})
const iterator = await client.traverse(DOMAIN_ACTIVITY, { _class: activity.class.Reaction })
try {
let processed = 0
while (true) {
const docs = await iterator.next(200)
if (docs === null || docs.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
for (const doc of docs) {
const reaction = doc as Reaction
const newCreateBy = socialIdByAccount[reaction.createBy] ?? reaction.createBy
if (newCreateBy === reaction.createBy) continue
operations.push({
filter: { _id: doc._id },
update: {
createBy: newCreateBy
}
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_ACTIVITY, operations)
}
processed += docs.length
ctx.info('...processed', { count: processed })
}
} finally {
await iterator.close()
}
ctx.info('finished processing activity reactions ', {})
}
export const activityOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, activityId, [
@@ -223,6 +277,10 @@ export const activityOperation: MigrateOperation = {
await client.move(DOMAIN_ACTIVITY, { _class: activity.class.Reaction }, DOMAIN_REACTION)
await client.move(DOMAIN_ACTIVITY, { _class: activity.class.UserMentionInfo }, DOMAIN_USER_MENTION)
}
},
{
state: 'accounts-to-social-ids',
func: migrateAccountsToSocialIds
}
])
},
+2 -59
View File
@@ -13,74 +13,17 @@
// limitations under the License.
//
import contact, { type Channel, type Person, type PersonAccount } from '@hcengineering/contact'
import core, {
DOMAIN_MODEL_TX,
type TxCUD,
type TxCreateDoc,
type Ref,
type TxUpdateDoc,
TxProcessor,
type Domain
} from '@hcengineering/core'
import { DOMAIN_CHANNEL, DOMAIN_CONTACT } from '@hcengineering/model-contact'
import {
tryMigrate,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient
} from '@hcengineering/model'
import aiBot, { aiBotId } from '@hcengineering/ai-bot'
const DOMAIN_ACTIVITY = 'activity' as Domain
async function migrateAiExtraAccounts (client: MigrationClient): Promise<void> {
const currentAccount = (
await client.model.findAll(contact.class.PersonAccount, { _id: aiBot.account.AIBot as Ref<PersonAccount> })
)[0]
if (currentAccount === undefined) return
const txes = await client.find<TxCUD<PersonAccount>>(DOMAIN_MODEL_TX, {
_class: { $in: [core.class.TxCreateDoc, core.class.TxUpdateDoc] },
objectClass: contact.class.PersonAccount,
objectId: aiBot.account.AIBot as Ref<PersonAccount>
})
const personsToDelete: Ref<Person>[] = []
const txesToDelete: Ref<TxCUD<PersonAccount>>[] = []
for (const tx of txes) {
if (tx._class === core.class.TxCreateDoc) {
const acc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<PersonAccount>)
if (acc.person !== currentAccount.person) {
personsToDelete.push(acc.person)
txesToDelete.push(tx._id)
}
} else if (tx._class === core.class.TxUpdateDoc) {
const person = (tx as TxUpdateDoc<PersonAccount>).operations.person
if (person !== undefined && person !== currentAccount.person) {
personsToDelete.push(person)
txesToDelete.push(tx._id)
}
}
}
if (personsToDelete.length === 0) return
await client.deleteMany(DOMAIN_MODEL_TX, { _id: { $in: txesToDelete } })
await client.deleteMany(DOMAIN_ACTIVITY, { attachedTo: { $in: personsToDelete } })
await client.deleteMany<Channel>(DOMAIN_CHANNEL, { attachedTo: { $in: personsToDelete } })
await client.deleteMany(DOMAIN_CONTACT, { _id: { $in: personsToDelete } })
}
import { aiBotId } from '@hcengineering/ai-bot'
export const aiBotOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, aiBotId, [
{
state: 'remove-ai-bot-extra-accounts-v100',
func: migrateAiExtraAccounts
}
])
await tryMigrate(client, aiBotId, [])
},
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {}
}
+4 -4
View File
@@ -18,7 +18,7 @@ import { type OnboardingChannel } from '@hcengineering/analytics-collector'
import chunter from '@hcengineering/chunter'
import { TChannel } from '@hcengineering/model-chunter'
import activity, { type ActivityMessageControl } from '@hcengineering/activity'
import core from '@hcengineering/core'
import core, { type PersonId, type WorkspaceUuid } from '@hcengineering/core'
import analyticsCollector from './plugin'
@@ -40,9 +40,9 @@ export class TOnboardingChannel extends TChannel implements OnboardingChannel {
@ReadOnly()
userName!: string
@Prop(TypeString(), analyticsCollector.string.Email)
@Prop(TypeString(), analyticsCollector.string.SocialId)
@ReadOnly()
email!: string
socialString!: PersonId
@Prop(TypeString(), analyticsCollector.string.WorkspaceName)
@ReadOnly()
@@ -54,7 +54,7 @@ export class TOnboardingChannel extends TChannel implements OnboardingChannel {
@Prop(TypeString(), analyticsCollector.string.WorkspaceId)
@ReadOnly()
workspaceId!: string
workspaceId!: WorkspaceUuid
@Prop(TypeBoolean(), analyticsCollector.string.DisableAIReplies)
disableAIReplies!: boolean
+51 -2
View File
@@ -15,14 +15,17 @@
import {
type MigrateOperation,
type MigrateUpdate,
type MigrationClient,
type MigrationDocumentQuery,
type MigrationUpgradeClient,
tryMigrate
} from '@hcengineering/model'
import { analyticsCollectorId } from '@hcengineering/analytics-collector'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
import analyticsCollector, { analyticsCollectorId } from '@hcengineering/analytics-collector'
import { DOMAIN_SPACE, getSocialKeyByOldEmail } from '@hcengineering/model-core'
import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION } from '@hcengineering/model-notification'
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import { buildSocialIdString, type Doc, MeasureMetricsContext } from '@hcengineering/core'
async function removeOnboardingChannels (client: MigrationClient): Promise<void> {
const channels = await client.find(DOMAIN_SPACE, { 'analytics:mixin:AnalyticsChannel': { $exists: true } })
@@ -41,12 +44,58 @@ async function removeOnboardingChannels (client: MigrationClient): Promise<void>
await client.deleteMany(DOMAIN_SPACE, { _id: { $in: channelsIds } })
}
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('analytics collector migrateAccountsToSocialIds', {})
ctx.info('processing analytics collector onboarding channels ', {})
const iterator = await client.traverse(DOMAIN_SPACE, { _class: analyticsCollector.class.OnboardingChannel })
try {
let processed = 0
while (true) {
const docs = await iterator.next(200)
if (docs === null || docs.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
for (const doc of docs) {
const email = (doc as any).email
if (email === undefined || email === '') continue
const socialString = buildSocialIdString(getSocialKeyByOldEmail(email))
operations.push({
filter: { _id: doc._id },
update: {
socialString
}
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_SPACE, operations)
}
processed += docs.length
ctx.info('...processed', { count: processed })
}
} finally {
await iterator.close()
}
ctx.info('finished processing analytics collector onboarding channels ', {})
}
export const analyticsCollectorOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, analyticsCollectorId, [
{
state: 'remove-analytics-channels-v3',
func: removeOnboardingChannels
},
{
state: 'accounts-to-social-ids',
func: migrateAccountsToSocialIds
}
])
},
+96 -3
View File
@@ -13,20 +13,109 @@
// limitations under the License.
//
import { calendarId, type Event, type ReccuringEvent } from '@hcengineering/calendar'
import { type Ref, type Space } from '@hcengineering/core'
import { type Calendar, calendarId, type Event, type ReccuringEvent } from '@hcengineering/calendar'
import { type Doc, MeasureMetricsContext, type PersonId, type Ref, type Space } from '@hcengineering/core'
import {
createDefaultSpace,
type MigrateUpdate,
type MigrationDocumentQuery,
tryMigrate,
tryUpgrade,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient
} from '@hcengineering/model'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
import { DOMAIN_SPACE, getSocialIdByOldAccount } from '@hcengineering/model-core'
import { DOMAIN_CALENDAR, DOMAIN_EVENT } from '.'
import calendar from './plugin'
function getCalendarId (socialString: PersonId): Ref<Calendar> {
return `${socialString}_calendar` as Ref<Calendar>
}
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('calendar migrateAccountsToSocialIds', {})
const hierarchy = client.hierarchy
const socialIdByAccount = await getSocialIdByOldAccount(client)
const eventClasses = hierarchy.getDescendants(calendar.class.Event)
const calendars = await client.find<Calendar>(DOMAIN_CALENDAR, {
_class: calendar.class.Calendar
})
ctx.info('processing internal calendars')
for (const calendar of calendars) {
const id = calendar._id
if (!id.endsWith('_calendar')) {
ctx.warn('Wrong calendar id format', { calendar: calendar._id })
continue
}
const account = id.substring(0, id.length - 9)
const socialId = socialIdByAccount[account]
if (socialId === undefined) {
ctx.warn('no socialId for account', { account })
continue
}
await client.delete(DOMAIN_CALENDAR, calendar._id)
await client.create(DOMAIN_CALENDAR, {
...calendar,
_id: getCalendarId(socialId)
})
}
let processedEvents = 0
const eventsIterator = await client.traverse<Event>(DOMAIN_EVENT, {
_class: { $in: eventClasses }
})
try {
while (true) {
const events = await eventsIterator.next(200)
if (events === null || events.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
for (const event of events) {
const id = event.calendar
if (!id.endsWith('_calendar')) {
// Nothing to do, in external calendar
continue
}
const account = id.substring(0, id.length - 9)
const socialId = socialIdByAccount[account]
if (socialId === undefined) {
ctx.warn('no socialId for account', { account })
continue
}
operations.push({
filter: { _id: event._id },
update: {
calendar: getCalendarId(socialId)
}
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_EVENT, operations)
}
processedEvents += events.length
ctx.info('...processed events', { count: processedEvents })
}
ctx.info('finished processing events')
} finally {
await eventsIterator.close()
}
}
async function migrateCalendars (client: MigrationClient): Promise<void> {
await client.move(
DOMAIN_SPACE,
@@ -146,6 +235,10 @@ export const calendarOperation: MigrateOperation = {
DOMAIN_EVENT
)
}
},
{
state: 'accounts-to-social-ids',
func: migrateAccountsToSocialIds
}
])
},
+25 -80
View File
@@ -13,9 +13,9 @@
// limitations under the License.
//
import { chunterId, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter'
import { chunterId, type ThreadMessage } from '@hcengineering/chunter'
import core, {
type Account,
type PersonId,
TxOperations,
type Class,
type Doc,
@@ -33,10 +33,14 @@ import {
} from '@hcengineering/model'
import activity, { migrateMessagesSpace, DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import notification from '@hcengineering/notification'
import contactPlugin, { type Person, type PersonAccount } from '@hcengineering/contact'
import {
getAllEmployeesPrimarySocialStrings,
pickPrimarySocialId,
getSocialStringsByEmployee,
includesAny
} from '@hcengineering/contact'
import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION } from '@hcengineering/model-notification'
import { type DocUpdateMessage } from '@hcengineering/activity'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
import chunter from './plugin'
import { DOMAIN_CHUNTER } from './index'
@@ -50,20 +54,20 @@ export async function createDocNotifyContexts (
objectClass: Ref<Class<Doc>>,
objectSpace: Ref<Space>
): Promise<void> {
const users = await client.findAll(core.class.Account, {})
const socialStringsByEmployee = getSocialStringsByEmployee(tx)
const allSocialStrings = Object.values(socialStringsByEmployee).flat()
const docNotifyContexts = await client.findAll(notification.class.DocNotifyContext, {
user: { $in: users.map((it) => it._id) },
user: { $in: allSocialStrings },
objectId
})
for (const user of users) {
if (user._id === core.account.System) {
continue
}
const docNotifyContext = docNotifyContexts.find((it) => it.user === user._id)
for (const userSocialStrings of Object.values(socialStringsByEmployee)) {
const docNotifyContext = docNotifyContexts.find((it) => userSocialStrings.includes(it.user))
if (docNotifyContext === undefined) {
await tx.createDoc(notification.class.DocNotifyContext, core.space.Space, {
user: user._id,
user: pickPrimarySocialId(userSocialStrings),
objectId,
objectClass,
objectSpace,
@@ -98,7 +102,7 @@ export async function createGeneral (client: MigrationUpgradeClient, tx: TxOpera
topic: 'General Channel',
private: false,
archived: false,
members: await getAllPersonAccounts(tx),
members: await getAllEmployeesPrimarySocialStrings(tx),
autoJoin: true
},
chunter.space.General
@@ -109,22 +113,16 @@ export async function createGeneral (client: MigrationUpgradeClient, tx: TxOpera
await createDocNotifyContexts(client, tx, chunter.space.General, chunter.class.Channel, core.space.Space)
}
async function getAllPersonAccounts (tx: TxOperations): Promise<Ref<PersonAccount>[]> {
const employees = await tx.findAll(contactPlugin.mixin.Employee, { active: true })
const accounts = await tx.findAll(contactPlugin.class.PersonAccount, {
person: { $in: employees.map((it) => it._id) }
})
return accounts.map((it) => it._id)
}
async function joinEmployees (current: Space, tx: TxOperations): Promise<void> {
const accs = await getAllPersonAccounts(tx)
const newMembers: Ref<Account>[] = [...current.members]
for (const acc of accs) {
if (!newMembers.includes(acc)) {
newMembers.push(acc)
const byEmployee = await getSocialStringsByEmployee(tx)
const newMembers: PersonId[] = [...current.members]
for (const socialStrings of Object.values(byEmployee)) {
if (!includesAny(newMembers, socialStrings)) {
newMembers.push(pickPrimarySocialId(socialStrings))
}
}
await tx.update(current, {
members: newMembers
})
@@ -154,7 +152,7 @@ export async function createRandom (client: MigrationUpgradeClient, tx: TxOperat
topic: 'Random Talks',
private: false,
archived: false,
members: await getAllPersonAccounts(tx),
members: await getAllEmployeesPrimarySocialStrings(tx),
autoJoin: true
},
chunter.space.Random
@@ -241,53 +239,6 @@ async function removeWrongActivity (client: MigrationClient): Promise<void> {
})
}
async function removeDuplicatedDirects (client: MigrationClient): Promise<void> {
const directs = await client.find<DirectMessage>(DOMAIN_SPACE, { _class: chunter.class.DirectMessage })
const personAccounts = await client.model.findAll<PersonAccount>(contactPlugin.class.PersonAccount, {})
const personByAccount = new Map(personAccounts.map((it) => [it._id, it.person]))
const accountsToPersons = (members: Ref<Account>[]): Ref<Person>[] => {
const personsSet = new Set(
members
.map((it) => personByAccount.get(it as Ref<PersonAccount>))
.filter((it): it is Ref<Person> => it !== undefined)
)
return Array.from(personsSet)
}
const map: Map<string, DirectMessage[]> = new Map<string, DirectMessage[]>()
const toRemove: Ref<DirectMessage>[] = []
for (const direct of directs) {
const persons = accountsToPersons(direct.members)
if (persons.length === 0) {
toRemove.push(direct._id)
continue
}
const key = persons.sort().join(',')
if (!map.has(key)) {
map.set(key, [direct])
} else {
map.get(key)?.push(direct)
}
}
for (const [, directs] of map) {
if (directs.length === 1) continue
const toSave = directs.reduce((acc, it) => ((it.messages ?? 0) > (acc.messages ?? 0) ? it : acc), directs[0])
const rest = directs.filter((it) => it._id !== toSave._id)
toRemove.push(...rest.map((it) => it._id))
}
await client.deleteMany(DOMAIN_SPACE, { _id: { $in: toRemove } })
await client.deleteMany(DOMAIN_ACTIVITY, { attachedTo: { $in: toRemove } })
await client.deleteMany(DOMAIN_ACTIVITY, { objectId: { $in: toRemove } })
await client.deleteMany(DOMAIN_DOC_NOTIFY, { objectId: { $in: toRemove } })
}
export const chunterOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, chunterId, [
@@ -346,12 +297,6 @@ export const chunterOperation: MigrateOperation = {
await client.deleteMany(DOMAIN_TX, { mixin: 'chunter:mixin:ChannelInfo' })
}
},
{
state: 'remove-duplicated-directs-v1',
func: async (client) => {
await removeDuplicatedDirects(client)
}
},
{
state: 'remove-direct-members-messages',
func: async (client) => {
+59 -41
View File
@@ -19,6 +19,7 @@ import {
AvatarType,
contactId,
type AvatarProvider,
type SocialIdentity,
type Channel,
type ChannelProvider,
type Contact,
@@ -28,7 +29,6 @@ import {
type Member,
type Organization,
type Person,
type PersonAccount,
type Status,
type PersonSpace
} from '@hcengineering/contact'
@@ -37,15 +37,19 @@ import {
DOMAIN_MODEL,
DateRangeMode,
IndexKind,
type Collection,
type Blob,
type Class,
type MarkupBlobRef,
type Domain,
type Ref,
type Timestamp
type Timestamp,
type SocialIdType,
type PersonUuid,
type PersonId
} from '@hcengineering/core'
import {
Collection,
Collection as CollectionType,
Hidden,
Index,
Mixin,
@@ -56,6 +60,7 @@ import {
TypeBoolean,
TypeCollaborativeDoc,
TypeDate,
TypePersonId,
TypeRecord,
TypeRef,
TypeString,
@@ -65,14 +70,14 @@ import {
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter'
import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@hcengineering/model-core'
import core, { TAttachedDoc, TDoc, TSpace } from '@hcengineering/model-core'
import { createPublicLinkAction } from '@hcengineering/model-guest'
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
import presentation from '@hcengineering/model-presentation'
import view, { createAction, createAttributePresenter, type Viewlet } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import { getEmbeddedLabel, type Asset, type IntlString, type Resource } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import templates from '@hcengineering/templates'
import { type AnyComponent } from '@hcengineering/ui/src/types'
@@ -125,18 +130,20 @@ export class TContact extends TDoc implements Contact {
url?: string
}
@Prop(Collection(contact.class.Channel), contact.string.ContactInfo)
@Prop(CollectionType(contact.class.Channel), contact.string.ContactInfo)
channels?: number
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
@Prop(CollectionType(attachment.class.Attachment), attachment.string.Attachments, {
shortLabel: attachment.string.Files
})
attachments?: number
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
@Prop(CollectionType(chunter.class.ChatMessage), chunter.string.Comments)
comments?: number
@Prop(TypeString(), contact.string.Location)
@Index(IndexKind.FullText)
city!: string
city?: string
}
@Model(contact.class.Channel, core.class.AttachedDoc, DOMAIN_CHANNEL)
@@ -156,11 +163,40 @@ export class TChannel extends TAttachedDoc implements Channel {
lastMessage?: Timestamp
}
@Model(contact.class.SocialIdentity, core.class.AttachedDoc, DOMAIN_CHANNEL)
@UX(contact.string.SocialId)
export class TSocialIdentity extends TAttachedDoc implements SocialIdentity {
declare attachedTo: Ref<Person>
declare attachedToClass: Ref<Class<Person>>
@Prop(TypePersonId(), getEmbeddedLabel('Key'))
@Hidden()
key!: PersonId
@Prop(TypeString(), contact.string.Type)
type!: SocialIdType
@Prop(TypeString(), contact.string.Value)
@Index(IndexKind.FullText)
value!: string
@Prop(TypeBoolean(), contact.string.Confirmed)
@ReadOnly()
confirmed!: boolean
}
@Model(contact.class.Person, contact.class.Contact)
@UX(contact.string.Person, contact.icon.Person, 'PRSN', 'name', undefined, contact.string.Persons)
export class TPerson extends TContact implements Person {
@Prop(TypeString(), getEmbeddedLabel('UUID'))
@Hidden()
personUuid?: PersonUuid
@Prop(TypeDate(DateRangeMode.DATE, false), contact.string.Birthday)
birthday?: Timestamp
@Prop(CollectionType(contact.class.SocialIdentity), contact.string.SocialIds)
socialIds?: Collection<SocialIdentity>
}
@Model(contact.class.Member, core.class.AttachedDoc, DOMAIN_CONTACT)
@@ -177,7 +213,7 @@ export class TOrganization extends TContact implements Organization {
@Index(IndexKind.FullText)
description!: MarkupBlobRef | null
@Prop(Collection(contact.class.Member), contact.string.Members)
@Prop(CollectionType(contact.class.Member), contact.string.Members)
members!: number
}
@@ -198,7 +234,7 @@ export class TEmployee extends TPerson implements Employee {
@Hidden()
active!: boolean
@Prop(Collection(contact.class.Status), contact.string.Status)
@Prop(CollectionType(contact.class.Status), contact.string.Status)
@Hidden()
statuses?: number
@@ -207,12 +243,6 @@ export class TEmployee extends TPerson implements Employee {
position?: string | null
}
@Model(contact.class.PersonAccount, core.class.Account)
export class TPersonAccount extends TAccount implements PersonAccount {
@Prop(TypeRef(contact.class.Person), contact.string.Person)
person!: Ref<Person>
}
@Model(contact.class.ContactsTab, core.class.Doc, DOMAIN_MODEL)
export class TContactsTab extends TDoc implements ContactsTab {
label!: IntlString
@@ -233,9 +263,9 @@ export function createModel (builder: Builder): void {
TChannelProvider,
TContact,
TPerson,
TSocialIdentity,
TOrganization,
TEmployee,
TPersonAccount,
TChannel,
TStatus,
TMember,
@@ -508,15 +538,15 @@ export function createModel (builder: Builder): void {
pinned: true
})
builder.mixin(core.class.Account, core.class.Class, view.mixin.Aggregation, {
createAggregationManager: contact.aggregation.CreatePersonAggregationManager,
setStoreFunc: contact.function.SetPersonStore,
filterFunc: contact.function.PersonFilterFunction
})
// builder.mixin(core.class.Account, core.class.Class, view.mixin.Aggregation, {
// createAggregationManager: contact.aggregation.CreatePersonAggregationManager,
// setStoreFunc: contact.function.SetPersonStore,
// filterFunc: contact.function.PersonFilterFunction
// })
builder.mixin(core.class.Account, core.class.Class, view.mixin.Groupping, {
grouppingManager: contact.aggregation.GrouppingPersonManager
})
// builder.mixin(core.class.Account, core.class.Class, view.mixin.Groupping, {
// grouppingManager: contact.aggregation.GrouppingPersonManager
// })
builder.mixin(contact.class.Organization, core.class.Class, view.mixin.ObjectEditor, {
editor: contact.component.EditOrganization,
@@ -583,8 +613,8 @@ export function createModel (builder: Builder): void {
presenter: contact.component.EmployeeFilterValuePresenter
})
builder.mixin(core.class.Account, core.class.Class, view.mixin.AttributeFilterPresenter, {
presenter: contact.component.PersonAccountFilterValuePresenter
builder.mixin(contact.class.Person, core.class.Class, view.mixin.AttributeFilterPresenter, {
presenter: contact.component.PersonFilterValuePresenter
})
builder.mixin(contact.mixin.Employee, core.class.Class, view.mixin.AttributeFilter, {
@@ -751,22 +781,10 @@ export function createModel (builder: Builder): void {
presenter: contact.component.PersonPresenter
})
builder.mixin(core.class.Account, core.class.Class, view.mixin.ArrayEditor, {
builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.ArrayEditor, {
inlineEditor: contact.component.AccountArrayEditor
})
builder.mixin(contact.class.PersonAccount, core.class.Class, view.mixin.ArrayEditor, {
inlineEditor: contact.component.AccountArrayEditor
})
builder.mixin(core.class.Account, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.PersonAccountPresenter
})
builder.mixin(core.class.Account, core.class.Class, view.mixin.AttributePresenter, {
presenter: contact.component.PersonAccountRefPresenter,
arrayPresenter: contact.component.AccountArrayEditor
})
builder.mixin(contact.class.Organization, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.OrganizationPresenter
})
+78 -81
View File
@@ -1,15 +1,19 @@
//
import { AvatarType, type Contact, type Person, type PersonSpace } from '@hcengineering/contact'
import { AvatarType, type SocialIdentity, type Contact } from '@hcengineering/contact'
import {
type AccountRole,
buildSocialIdString,
type Class,
type Doc,
type Domain,
DOMAIN_MODEL_TX,
DOMAIN_TX,
generateId,
MeasureMetricsContext,
type Ref,
type Space,
TxOperations
type TxCUD
} from '@hcengineering/core'
import {
createDefaultSpace,
@@ -23,42 +27,10 @@ import {
tryUpgrade
} from '@hcengineering/model'
import activity, { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import core, { getAccountsFromTxes, getSocialKeyByOldEmail } from '@hcengineering/model-core'
import { DOMAIN_VIEW } from '@hcengineering/model-view'
import contact, { contactId, DOMAIN_CONTACT } from './index'
async function createEmployeeEmail (client: TxOperations): Promise<void> {
const employees = await client.findAll(contact.mixin.Employee, {})
const channels = (
await client.findAll(contact.class.Channel, {
attachedTo: { $in: employees.map((p) => p._id) }
})
).filter((it) => it.provider === contact.channelProvider.Email)
const channelsMap = new Map(channels.map((p) => [p.attachedTo, p]))
for (const employee of employees) {
const acc = client.getModel().getAccountByPersonId(employee._id)
if (acc.length === 0) continue
const current = channelsMap.get(employee._id)
if (current === undefined) {
await client.addCollection(
contact.class.Channel,
contact.space.Contacts,
employee._id,
contact.mixin.Employee,
'channels',
{
provider: contact.channelProvider.Email,
value: acc[0].email.trim()
},
undefined,
employee.modifiedOn
)
} else if (current.value !== acc[0].email.trim()) {
await client.update(current, { value: acc[0].email.trim() }, false, current.modifiedOn)
}
}
}
import contact, { contactId, DOMAIN_CHANNEL, DOMAIN_CONTACT } from './index'
const colorPrefix = 'color://'
const gravatarPrefix = 'gravatar://'
@@ -113,47 +85,75 @@ async function migrateAvatars (client: MigrationClient): Promise<void> {
)
}
async function createPersonSpaces (client: MigrationClient): Promise<void> {
const spaces = await client.find<PersonSpace>(DOMAIN_SPACE, { _class: contact.class.PersonSpace })
async function getOldPersonAccounts (
client: MigrationClient
): Promise<Array<{ person: any, email: string, role: AccountRole }>> {
const accountsTxes: TxCUD<Doc>[] = await client.find<TxCUD<Doc>>(DOMAIN_MODEL_TX, {
objectClass: 'contact:class:PersonAccount' as Ref<Class<Doc>>
})
if (spaces.length > 0) {
return
}
return getAccountsFromTxes(accountsTxes)
}
const accounts = await client.model.findAll(contact.class.PersonAccount, {})
const employees = await client.find(DOMAIN_CONTACT, { [contact.mixin.Employee]: { $exists: true } })
const newSpaces = new Map<Ref<Person>, PersonSpace>()
const now = Date.now()
for (const account of accounts) {
const employee = employees.find(({ _id }) => _id === account.person)
if (employee === undefined) continue
const space = newSpaces.get(account.person)
if (space !== undefined) {
space.members.push(account._id)
} else {
newSpaces.set(account.person, {
_id: generateId(),
_class: contact.class.PersonSpace,
space: core.space.Space,
name: 'Personal space',
description: '',
private: true,
archived: false,
members: [account._id],
person: account.person,
modifiedBy: core.account.System,
createdBy: core.account.System,
modifiedOn: now,
createdOn: now
})
async function assignWorkspaceRoles (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('contact assignWorkspaceRoles', {})
ctx.info('assigning workspace roles...')
const oldPersonAccounts = await getOldPersonAccounts(client)
for (const { person, email, role } of oldPersonAccounts) {
// check it's an active employee
const personObj = (await client.find(DOMAIN_CONTACT, { _id: person, _class: contact.class.Person }))[0]
if (personObj === undefined) {
continue
}
const employee = client.hierarchy.as(personObj, contact.mixin.Employee)
if (employee === undefined || !employee.active) {
continue
}
const socialKey = getSocialKeyByOldEmail(email)
try {
await client.accountClient.updateWorkspaceRoleBySocialId(buildSocialIdString(socialKey), role)
} catch (err: any) {
ctx.error('Failed to update workspace role', { email, ...socialKey, role, err })
}
}
await client.create(DOMAIN_SPACE, Array.from(newSpaces.values()))
ctx.info('finished assigning workspace roles', { users: oldPersonAccounts.length })
}
async function createSocialIdentities (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('createSocialIdentities', {})
ctx.info('processing person accounts ', {})
const personAccountsTxes: any[] = await client.find<TxCUD<Doc>>(DOMAIN_MODEL_TX, {
objectClass: 'contact:class:PersonAccount' as Ref<Class<Doc>>
})
const personAccounts = getAccountsFromTxes(personAccountsTxes)
for (const pAcc of personAccounts) {
const email: string = pAcc.email ?? ''
if (email === '') continue
const socialIdKey = getSocialKeyByOldEmail(email)
const socialId: SocialIdentity = {
_id: generateId(),
_class: contact.class.SocialIdentity,
space: contact.space.Contacts,
...socialIdKey,
key: buildSocialIdString(socialIdKey),
confirmed: false,
attachedTo: pAcc.person,
attachedToClass: contact.class.Person,
collection: 'socialIds',
modifiedOn: Date.now(),
createdBy: core.account.ConfigUser,
createdOn: Date.now(),
modifiedBy: core.account.ConfigUser
}
await client.create(DOMAIN_CHANNEL, socialId)
}
}
export const contactOperation: MigrateOperation = {
@@ -298,8 +298,12 @@ export const contactOperation: MigrateOperation = {
}
},
{
state: 'create-person-spaces-v1',
func: createPersonSpaces
state: 'create-social-identities',
func: createSocialIdentities
},
{
state: 'assign-workspace-roles',
func: assignWorkspaceRoles
}
])
},
@@ -310,13 +314,6 @@ export const contactOperation: MigrateOperation = {
func: async (client) => {
await createDefaultSpace(client, contact.space.Contacts, { name: 'Contacts', description: 'Contacts' })
}
},
{
state: 'createEmails',
func: async (client) => {
const tx = new TxOperations(client, core.account.System)
await createEmployeeEmail(tx)
}
}
])
}
-3
View File
@@ -39,8 +39,6 @@ export default mergeIds(contactId, contact, {
OrganizationPresenter: '' as AnyComponent,
Contacts: '' as AnyComponent,
ContactsTabs: '' as AnyComponent,
PersonAccountPresenter: '' as AnyComponent,
PersonAccountRefPresenter: '' as AnyComponent,
OrganizationEditor: '' as AnyComponent,
EmployeePresenter: '' as AnyComponent,
EmployeeRefPresenter: '' as AnyComponent,
@@ -59,7 +57,6 @@ export default mergeIds(contactId, contact, {
ActivityChannelPresenter: '' as AnyComponent,
EmployeeFilter: '' as AnyComponent,
EmployeeFilterValuePresenter: '' as AnyComponent,
PersonAccountFilterValuePresenter: '' as AnyComponent,
ChannelIcon: '' as AnyComponent
},
string: {
+5 -3
View File
@@ -34,7 +34,8 @@ import {
type Ref,
SortingOrder,
toIdMap,
TxOperations
TxOperations,
type WorkspaceDataId
} from '@hcengineering/core'
import {
createDefaultSpace,
@@ -300,7 +301,8 @@ async function migrateDocSections (client: MigrationClient): Promise<void> {
// Migrate sections headers + content
try {
const collabId = makeDocCollabId(document, 'content')
const ydoc = await loadCollabYdoc(ctx, storage, client.workspaceId, collabId)
const dataId = client.wsIds.dataId ?? (client.wsIds.uuid as unknown as WorkspaceDataId)
const ydoc = await loadCollabYdoc(ctx, storage, dataId, collabId)
if (ydoc === undefined) {
// no content, ignore
continue
@@ -346,7 +348,7 @@ async function migrateDocSections (client: MigrationClient): Promise<void> {
}
})
await saveCollabYdoc(ctx, storage, client.workspaceId, collabId, ydoc)
await saveCollabYdoc(ctx, storage, dataId, collabId, ydoc)
} catch (err) {
ctx.error('error collaborative document content migration', { error: err, document: document.title })
}
+3 -3
View File
@@ -55,9 +55,9 @@ import {
type CollectionSize,
type Role,
type TypedSpace,
type Account,
type RolesAssignment,
type Rank
type Rank,
type PersonId
} from '@hcengineering/core'
import {
ArrOf,
@@ -472,7 +472,7 @@ export class TDocumentApprovalRequest extends TDocumentRequest implements Docume
@Mixin(documents.mixin.DocumentSpaceTypeData, documents.class.DocumentSpace)
@UX(getEmbeddedLabel('Default Documents'), documents.icon.Document)
export class TDocumentSpaceTypeData extends TDocumentSpace implements RolesAssignment {
[key: Ref<Role>]: Ref<Account>[]
[key: Ref<Role>]: PersonId[]
}
/**
+10 -5
View File
@@ -14,7 +14,7 @@
//
import {
type Account,
type PersonId,
type AnyAttribute,
type ArrOf,
type Association,
@@ -71,6 +71,7 @@ import {
TypeRef,
TypeString,
TypeTimestamp,
TypePersonId,
UX
} from '@hcengineering/model'
import { getEmbeddedLabel, type IntlString, type Plugin } from '@hcengineering/platform'
@@ -102,13 +103,13 @@ export class TDoc extends TObj implements Doc {
@Index(IndexKind.Indexed)
modifiedOn!: Timestamp
@Prop(TypeRef(core.class.Account), core.string.ModifiedBy)
@Prop(TypePersonId(), core.string.ModifiedBy)
@Index(IndexKind.Indexed)
modifiedBy!: Ref<Account>
modifiedBy!: PersonId
@Prop(TypeRef(core.class.Account), core.string.CreatedBy)
@Prop(TypePersonId(), core.string.CreatedBy)
@Index(IndexKind.Indexed)
createdBy!: Ref<Account>
createdBy!: PersonId
@Prop(TypeTimestamp(), core.string.CreatedDate)
@ReadOnly()
@@ -260,6 +261,10 @@ export class TTypeFileSize extends TType {}
@Model(core.class.TypeMarkup, core.class.Type)
export class TTypeMarkup extends TType {}
@UX(core.string.PersonId)
@Model(core.class.TypePersonId, core.class.Type)
export class TTypePersonId extends TType {}
@UX(core.string.Ref)
@Model(core.class.RefTo, core.class.Type)
export class TRefTo extends TType implements RefTo<Doc> {
+5 -25
View File
@@ -14,7 +14,6 @@
//
import {
AccountRole,
DOMAIN_BENCHMARK,
DOMAIN_BLOB,
DOMAIN_CONFIGURATION,
@@ -23,8 +22,7 @@ import {
DOMAIN_SPACE,
DOMAIN_STATUS,
DOMAIN_TRANSIENT,
DOMAIN_TX,
systemAccountEmail
DOMAIN_TX
} from '@hcengineering/core'
import { type Builder } from '@hcengineering/model'
import { TBenchmarkDoc } from './benchmark'
@@ -64,6 +62,7 @@ import {
TTypeHyperlink,
TTypeIntlString,
TTypeMarkup,
TTypePersonId,
TTypeNumber,
TTypeRank,
TTypeRecord,
@@ -74,16 +73,7 @@ import {
TSequence
} from './core'
import { definePermissions } from './permissions'
import {
TAccount,
TPermission,
TRole,
TSpace,
TSpaceType,
TSpaceTypeDescriptor,
TSystemSpace,
TTypedSpace
} from './security'
import { TPermission, TRole, TSpace, TSpaceType, TSpaceTypeDescriptor, TSystemSpace, TTypedSpace } from './security'
import { defineSpaceType } from './spaceType'
import { TDomainStatusPlaceholder, TStatus, TStatusCategory } from './status'
import { TUserStatus } from './transient'
@@ -91,7 +81,7 @@ import { TTx, TTxApplyIf, TTxCreateDoc, TTxCUD, TTxMixin, TTxRemoveDoc, TTxUpdat
export { coreId, DOMAIN_SPACE } from '@hcengineering/core'
export * from './core'
export { coreOperation } from './migration'
export { coreOperation, getSocialIdByOldAccount, getAccountsFromTxes, getSocialKeyByOldEmail } from './migration'
export * from './security'
export * from './status'
export * from './tx'
@@ -120,11 +110,11 @@ export function createModel (builder: Builder): void {
TSpaceTypeDescriptor,
TRole,
TPermission,
TAccount,
TAttribute,
TType,
TEnumOf,
TTypeMarkup,
TTypePersonId,
TTypeCollaborativeDoc,
TArrOf,
TRefTo,
@@ -164,16 +154,6 @@ export function createModel (builder: Builder): void {
TTransientConfiguration
)
builder.createDoc(
core.class.Account,
core.space.Model,
{
email: systemAccountEmail,
role: AccountRole.Owner
},
core.account.System
)
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_TX,
disabled: [
+247 -9
View File
@@ -15,6 +15,7 @@
import { saveCollabJson } from '@hcengineering/collaboration'
import core, {
buildSocialIdString,
coreId,
DOMAIN_MODEL_TX,
DOMAIN_SPACE,
@@ -26,6 +27,8 @@ import core, {
makeDocCollabId,
MeasureMetricsContext,
RateLimiter,
SocialIdType,
type PersonId,
type AnyAttribute,
type Blob,
type Class,
@@ -36,7 +39,15 @@ import core, {
type Space,
type Status,
type TxCreateDoc,
type TxCUD
type TxCUD,
type SpaceType,
type TxUpdateDoc,
type Role,
toIdMap,
type TypedSpace,
TxProcessor,
type SocialKey,
type WorkspaceDataId
} from '@hcengineering/core'
import {
createDefaultSpace,
@@ -226,7 +237,14 @@ async function processMigrateContentFor (
if (value != null && value.startsWith('{')) {
try {
const buffer = Buffer.from(value)
await storageAdapter.put(ctx, client.workspaceId, blobId, buffer, 'application/json', buffer.length)
await storageAdapter.put(
ctx,
client.wsIds.dataId ?? (client.wsIds.uuid as unknown as WorkspaceDataId),
blobId,
buffer,
'application/json',
buffer.length
)
} catch (err) {
ctx.error('failed to process document', { _class: doc._class, _id: doc._id, err })
}
@@ -284,6 +302,221 @@ async function migrateCollaborativeDocsToJson (client: MigrationClient): Promise
}
}
export function getAccountsFromTxes (accTxes: TxCUD<Doc>[]): any {
const byAccounts = accTxes.reduce<Record<string, TxCUD<Doc>[]>>((acc, tx) => {
if (acc[tx.objectId] === undefined) {
acc[tx.objectId] = []
}
acc[tx.objectId].push(tx)
return acc
}, {})
return Object.values(byAccounts)
.map((txes) => TxProcessor.buildDoc2Doc(txes))
.filter((it) => it !== undefined)
}
export async function getSocialIdByOldAccount (client: MigrationClient): Promise<Record<string, PersonId>> {
const systemAccounts = [core.account.System, core.account.ConfigUser]
const accountsTxes: TxCUD<Doc>[] = await client.find<TxCUD<Doc>>(DOMAIN_MODEL_TX, {
objectClass: { $in: ['core:class:Account', 'contact:class:PersonAccount'] as Ref<Class<Doc>>[] }
})
const accounts = getAccountsFromTxes(accountsTxes)
const socialIdByAccount: Record<string, PersonId> = {}
for (const account of accounts) {
if (account.email === undefined) {
continue
}
if (systemAccounts.includes(account._id)) {
socialIdByAccount[account._id] = account._id
} else {
socialIdByAccount[account._id] = buildSocialIdString(getSocialKeyByOldEmail(account.email))
}
}
return socialIdByAccount
}
export function getSocialKeyByOldEmail (rawEmail: string): SocialKey {
const email = rawEmail.toLowerCase()
let type: SocialIdType
let value: string
if (email.startsWith('github:')) {
type = SocialIdType.GITHUB
value = email.slice(7)
} else if (email.startsWith('openid:')) {
type = SocialIdType.OIDC
value = email.slice(7)
} else {
type = SocialIdType.EMAIL
value = email
}
return {
type,
value
}
}
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('core migrateAccountsToSocialIds', {})
const hierarchy = client.hierarchy
const socialIdByAccount = await getSocialIdByOldAccount(client)
ctx.info('migrating createdBy and modifiedBy')
function chunkArray<T> (array: T[], chunkSize: number): T[][] {
const chunks: T[][] = []
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize))
}
return chunks
}
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
for (const [accId, socialId] of Object.entries(socialIdByAccount)) {
if (accId === socialId) continue
operations.push({
filter: { createdBy: accId as any },
update: {
createdBy: socialId
}
})
operations.push({
filter: { modifiedBy: accId as any },
update: {
modifiedBy: socialId
}
})
}
if (operations.length > 0) {
const operationsChunks = chunkArray(operations, 40)
for (const domain of client.hierarchy.domains()) {
let processed = 0
ctx.info('processing domain ', { domain })
for (const operationsChunk of operationsChunks) {
if (operationsChunk.length === 0) continue
await client.bulk(domain, operationsChunk)
processed++
if (operationsChunks.length > 1) {
ctx.info('processed chunk', { processed, of: operationsChunks.length })
}
}
}
} else {
ctx.info('no user accounts to migrate')
}
ctx.info('finished migrating createdBy and modifiedBy')
const spaceTypes = client.model.findAllSync(core.class.SpaceType, {})
const spaceTypesById = toIdMap(spaceTypes)
const roles = client.model.findAllSync(core.class.Role, {})
const rolesBySpaceType = new Map<Ref<SpaceType>, Role[]>()
for (const role of roles) {
const spaceType = role.attachedTo
if (spaceType === undefined) continue
if (rolesBySpaceType.has(spaceType)) {
rolesBySpaceType.get(spaceType)?.push(role)
} else {
rolesBySpaceType.set(spaceType, [role])
}
}
ctx.info('processing spaces members, owners and roles assignment', {})
let processedSpaces = 0
const spacesIterator = await client.traverse(DOMAIN_SPACE, {})
try {
while (true) {
const spaces = await spacesIterator.next(200)
if (spaces === null || spaces.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<Space>, update: MigrateUpdate<Space> }[] = []
for (const s of spaces) {
if (!hierarchy.isDerived(s._class, core.class.Space)) continue
const space = s as Space
const newMembers = space.members.map((m) => socialIdByAccount[m] ?? m)
const newOwners = space.owners?.map((m) => socialIdByAccount[m] ?? m)
const update: MigrateUpdate<Space> = {
members: newMembers,
owners: newOwners
}
const type = spaceTypesById.get((space as TypedSpace).type)
if (type !== undefined) {
const mixin = hierarchy.as(space, type.targetClass)
if (mixin !== undefined) {
const roles = rolesBySpaceType.get(type._id)
for (const role of roles ?? []) {
const oldAssignees: string[] | undefined = (mixin as any)[role._id]
if (oldAssignees != null && oldAssignees.length > 0) {
const newAssignees = oldAssignees.map((a) => socialIdByAccount[a])
update[`${type.targetClass}`] = {
[role._id]: newAssignees
}
}
}
}
}
operations.push({
filter: { _id: space._id },
update
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_SPACE, operations)
}
processedSpaces += spaces.length
ctx.info('...spaces processed', { count: processedSpaces })
}
ctx.info('finished processing spaces members, owners and roles assignment', { processedSpaces })
} finally {
await spacesIterator.close()
}
ctx.info('processing space types members', {})
let updatedSpaceTypes = 0
for (const spaceType of spaceTypes) {
if (spaceType.members === undefined || spaceType.members.length === 0) continue
const newMembers = spaceType.members.map((m) => socialIdByAccount[m] ?? m)
const tx: TxUpdateDoc<SpaceType> = {
_id: generateId(),
_class: core.class.TxUpdateDoc,
space: core.space.Tx,
objectId: spaceType._id,
objectClass: spaceType._class,
objectSpace: spaceType.space,
operations: {
members: newMembers
},
modifiedOn: Date.now(),
createdBy: core.account.ConfigUser,
createdOn: Date.now(),
modifiedBy: core.account.ConfigUser
}
await client.create(DOMAIN_MODEL_TX, tx)
updatedSpaceTypes++
}
ctx.info('finished processing space types members', { totalSpaceTypes: spaceTypes.length, updatedSpaceTypes })
}
async function processMigrateJsonForDomain (
ctx: MeasureContext,
domain: Domain,
@@ -331,7 +564,7 @@ async function processMigrateJsonForDoc (
client: MigrationClient,
storageAdapter: StorageAdapter
): Promise<MigrateUpdate<Doc>> {
const { hierarchy, workspaceId } = client
const { hierarchy, wsIds } = client
const update: MigrateUpdate<Doc> = {}
@@ -349,12 +582,13 @@ async function processMigrateJsonForDoc (
: attribute.name
const collabId = makeDocCollabId(doc, attribute.name)
const dataId = wsIds.dataId ?? (wsIds.uuid as unknown as WorkspaceDataId)
if (value.startsWith('{')) {
// For some reason we have documents that are already markups
const jsonId = await retry(5, async () => {
return await saveCollabJson(ctx, storageAdapter, workspaceId, collabId, value)
return await saveCollabJson(ctx, storageAdapter, dataId, collabId, value)
})
update[attributeName] = jsonId
continue
}
@@ -374,17 +608,17 @@ async function processMigrateJsonForDoc (
const ydocId = makeCollabYdocId(collabId)
if (ydocId !== currentYdocId) {
await retry(5, async () => {
const stat = await storageAdapter.stat(ctx, workspaceId, currentYdocId)
const stat = await storageAdapter.stat(ctx, dataId, currentYdocId)
if (stat !== undefined) {
const data = await storageAdapter.read(ctx, workspaceId, currentYdocId)
const data = await storageAdapter.read(ctx, dataId, currentYdocId)
const buffer = Buffer.concat(data as any)
await storageAdapter.put(ctx, workspaceId, ydocId, buffer, 'application/ydoc', buffer.length)
await storageAdapter.put(ctx, dataId, ydocId, buffer, 'application/ydoc', buffer.length)
}
})
}
} catch (err) {
const error = err instanceof Error ? err.message : String(err)
ctx.warn('failed to process collaborative doc', { workspaceId, collabId, currentYdocId, error })
ctx.warn('failed to process collaborative doc', { dataId, collabId, currentYdocId, error })
}
const unset = update.$unset ?? {}
@@ -484,6 +718,10 @@ export const coreOperation: MigrateOperation = {
{
state: 'collaborative-docs-to-json',
func: migrateCollaborativeDocsToJson
},
{
state: 'accounts-to-social-ids',
func: migrateAccountsToSocialIds
}
])
},
+9 -16
View File
@@ -17,8 +17,7 @@ import {
DOMAIN_MODEL,
DOMAIN_SPACE,
IndexKind,
type Account,
type AccountRole,
type PersonId,
type Arr,
type Class,
type CollectionSize,
@@ -42,6 +41,7 @@ import {
TypeBoolean,
TypeRef,
TypeString,
TypePersonId,
UX
} from '@hcengineering/model'
import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform'
@@ -68,12 +68,12 @@ export class TSpace extends TDoc implements Space {
@Index(IndexKind.Indexed)
archived!: boolean
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Members)
@Prop(ArrOf(TypePersonId()), core.string.Members)
@Index(IndexKind.Indexed)
members!: Arr<Ref<Account>>
members!: Arr<PersonId>
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Owners)
owners?: Ref<Account>[]
@Prop(ArrOf(TypePersonId()), core.string.Owners)
owners?: PersonId[]
@Prop(TypeBoolean(), core.string.AutoJoin)
autoJoin?: boolean
@@ -119,8 +119,8 @@ export class TSpaceType extends TDoc implements SpaceType {
@Prop(Collection(core.class.Role), core.string.Roles)
roles!: CollectionSize<Role>
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Members)
members!: Arr<Ref<Account>>
@Prop(ArrOf(TypePersonId()), core.string.Members)
members!: Arr<PersonId>
@Prop(TypeBoolean(), core.string.AutoJoin)
autoJoin?: boolean
@@ -162,12 +162,5 @@ export class TPermission extends TDoc implements Permission {
@Mixin(core.mixin.SpacesTypeData, core.class.Space)
@UX(getEmbeddedLabel("All spaces' type")) // TODO: add icon?
export class TSpacesTypeData extends TSpace implements RolesAssignment {
[key: Ref<Role>]: Ref<Account>[]
}
@Model(core.class.Account, core.class.Doc, DOMAIN_MODEL)
@UX(core.string.Account, undefined, undefined, 'name')
export class TAccount extends TDoc implements Account {
email!: string
role!: AccountRole
[key: Ref<Role>]: PersonId[]
}
+2 -2
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { ArrOf, Prop, TypeRef, type Builder } from '@hcengineering/model'
import { ArrOf, Prop, TypeString, type Builder } from '@hcengineering/model'
import { type Asset } from '@hcengineering/platform'
import { getRoleAttributeLabel } from '@hcengineering/core'
@@ -31,7 +31,7 @@ const roles = [
export function defineSpaceType (builder: Builder): void {
for (const role of roles) {
const label = getRoleAttributeLabel(role.name)
const roleAssgtType = ArrOf(TypeRef(core.class.Account))
const roleAssgtType = ArrOf(TypeString())
Prop(roleAssgtType, label)(TSpacesTypeData.prototype, role._id)
}
+2 -2
View File
@@ -13,13 +13,13 @@
// limitations under the License.
//
import { DOMAIN_TRANSIENT, type Account, type Ref, type UserStatus } from '@hcengineering/core'
import { DOMAIN_TRANSIENT, type PersonUuid, type UserStatus } from '@hcengineering/core'
import { Model } from '@hcengineering/model'
import core from './component'
import { TDoc } from './core'
@Model(core.class.UserStatus, core.class.Doc, DOMAIN_TRANSIENT)
export class TUserStatus extends TDoc implements UserStatus {
user!: Ref<Account>
user!: PersonUuid
online!: boolean
}
+6 -5
View File
@@ -14,8 +14,8 @@
//
import activity from '@hcengineering/activity'
import type { CollectionSize, Domain, MarkupBlobRef, Rank, Role, RolesAssignment } from '@hcengineering/core'
import { Account, AccountRole, IndexKind, Ref } from '@hcengineering/core'
import type { CollectionSize, MarkupBlobRef, Domain, Rank, Ref, Role, RolesAssignment } from '@hcengineering/core'
import { PersonId, AccountRole, IndexKind } from '@hcengineering/core'
import {
type Document,
type DocumentSnapshot,
@@ -36,6 +36,7 @@ import {
TypeNumber,
TypeRef,
TypeString,
TypePersonId,
UX
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
@@ -79,9 +80,9 @@ export class TDocument extends TDoc implements Document, Todoable {
@Hidden()
declare space: Ref<Teamspace>
@Prop(TypeRef(core.class.Account), document.string.LockedBy)
@Prop(TypePersonId(), document.string.LockedBy)
@Hidden()
lockedBy?: Ref<Account>
lockedBy?: PersonId
@Prop(Collection(attachment.class.Embedding), attachment.string.Embeddings)
embeddings?: number
@@ -150,7 +151,7 @@ export class TTeamspace extends TTypedSpace implements Teamspace {}
@Mixin(document.mixin.DefaultTeamspaceTypeData, document.class.Teamspace)
@UX(getEmbeddedLabel('Default teamspace type'), document.icon.Document)
export class TDefaultTeamspaceTypeData extends TTeamspace implements RolesAssignment {
[key: Ref<Role>]: Ref<Account>[]
[key: Ref<Role>]: PersonId[]
}
function defineTeamspace (builder: Builder): void {
+58 -8
View File
@@ -18,11 +18,12 @@ import {
DOMAIN_TX,
makeDocCollabId,
MeasureMetricsContext,
type Ref,
SortingOrder,
type Class,
type CollaborativeDoc,
type Doc,
type Ref
type WorkspaceDataId
} from '@hcengineering/core'
import { type Document, type DocumentSnapshot, type Teamspace } from '@hcengineering/document'
import {
@@ -35,7 +36,7 @@ import {
type MigrationUpgradeClient
} from '@hcengineering/model'
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import core, { DOMAIN_SPACE, getSocialIdByOldAccount } from '@hcengineering/model-core'
import { DOMAIN_NOTIFICATION } from '@hcengineering/notification'
import { type Asset } from '@hcengineering/platform'
import { makeRank } from '@hcengineering/rank'
@@ -212,8 +213,8 @@ async function renameFieldsRevert (client: MigrationClient): Promise<void> {
try {
const collabId = makeDocCollabId(document, 'content')
const ydoc = await loadCollabYdoc(ctx, storage, client.workspaceId, collabId)
const dataId = client.wsIds.dataId ?? (client.wsIds.uuid as unknown as WorkspaceDataId)
const ydoc = await loadCollabYdoc(ctx, storage, dataId, collabId)
if (ydoc === undefined) {
continue
}
@@ -224,7 +225,7 @@ async function renameFieldsRevert (client: MigrationClient): Promise<void> {
yDocCopyXmlField(ydoc, 'description', 'content')
await saveCollabYdoc(ctx, storage, client.workspaceId, collabId, ydoc)
await saveCollabYdoc(ctx, storage, dataId, collabId, ydoc)
} catch (err) {
ctx.error('error document content migration', { error: err, document: document.title })
}
@@ -260,8 +261,8 @@ async function restoreContentField (client: MigrationClient): Promise<void> {
for (const document of documents) {
try {
const collabId = makeDocCollabId(document, 'content')
const ydoc = await loadCollabYdoc(ctx, storage, client.workspaceId, collabId)
const dataId = client.wsIds.dataId ?? (client.wsIds.uuid as unknown as WorkspaceDataId)
const ydoc = await loadCollabYdoc(ctx, storage, dataId, collabId)
if (ydoc === undefined) {
ctx.error('document content not found', { document: document.title })
continue
@@ -275,7 +276,7 @@ async function restoreContentField (client: MigrationClient): Promise<void> {
if (ydoc.share.has('')) {
yDocCopyXmlField(ydoc, '', 'content')
if (ydoc.share.has('content')) {
await saveCollabYdoc(ctx, storage, client.workspaceId, collabId, ydoc)
await saveCollabYdoc(ctx, storage, dataId, collabId, ydoc)
} else {
ctx.error('document content still not found', { document: document.title })
}
@@ -296,6 +297,51 @@ async function migrateRanks (client: MigrationClient): Promise<void> {
}
}
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('document migrateAccountsToSocialIds', {})
const socialIdByAccount = await getSocialIdByOldAccount(client)
ctx.info('processing document lockedBy ', {})
const iterator = await client.traverse(DOMAIN_DOCUMENT, { _class: document.class.Document })
try {
let processed = 0
while (true) {
const docs = await iterator.next(200)
if (docs === null || docs.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<Document>, update: MigrateUpdate<Document> }[] = []
for (const doc of docs) {
const document = doc as Document
const newLockedBy =
document.lockedBy != null ? socialIdByAccount[document.lockedBy] ?? document.lockedBy : document.lockedBy
if (newLockedBy === document.lockedBy) continue
operations.push({
filter: { _id: document._id },
update: {
lockedBy: newLockedBy
}
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_DOCUMENT, operations)
}
processed += docs.length
ctx.info('...processed', { count: processed })
}
} finally {
await iterator.close()
}
ctx.info('finished processing document lockedBy ', {})
}
async function removeOldClasses (client: MigrationClient): Promise<void> {
const classes = [
'document:class:DocumentContent',
@@ -355,6 +401,10 @@ export const documentOperation: MigrateOperation = {
{
state: 'migrateEmbeddings',
func: migrateEmbeddings
},
{
state: 'accounts-to-social-ids',
func: migrateAccountsToSocialIds
}
])
},
+2 -2
View File
@@ -24,7 +24,7 @@ import core, {
type Ref,
type Role,
type RolesAssignment,
Account,
PersonId,
AccountRole,
IndexKind,
SortingOrder
@@ -81,7 +81,7 @@ export class TDrive extends TTypedSpace implements Drive {}
@Mixin(drive.mixin.DefaultDriveTypeData, drive.class.Drive)
@UX(getEmbeddedLabel('Default drive type'))
export class TDefaultDriveTypeData extends TDrive implements RolesAssignment {
[key: Ref<Role>]: Ref<Account>[]
[key: Ref<Role>]: PersonId[]
}
@Model(drive.class.Resource, core.class.Doc, DOMAIN_DRIVE)
+2 -18
View File
@@ -1,12 +1,5 @@
import {
AccountRole,
type Class,
type IndexingConfiguration,
type Doc,
type Domain,
type Ref
} from '@hcengineering/core'
import { type PublicLink, type Restrictions, guestAccountEmail } from '@hcengineering/guest'
import { type Class, type IndexingConfiguration, type Doc, type Domain, type Ref } from '@hcengineering/core'
import { type PublicLink, type Restrictions } from '@hcengineering/guest'
import { type Builder, Model } from '@hcengineering/model'
import core, { TDoc } from '@hcengineering/model-core'
import { type Location } from '@hcengineering/ui'
@@ -26,15 +19,6 @@ export class TPublicLink extends TDoc implements PublicLink {
export function createModel (builder: Builder): void {
builder.createModel(TPublicLink)
builder.createDoc(
core.class.Account,
core.space.Model,
{
email: guestAccountEmail,
role: AccountRole.DocGuest
},
guest.account.Guest
)
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: GUEST_DOMAIN,
disabled: [
+1 -64
View File
@@ -1,78 +1,15 @@
import {
AccountRole,
DOMAIN_MODEL_TX,
type Account,
type Ref,
type Space,
type TxCreateDoc,
type TxUpdateDoc
} from '@hcengineering/core'
import { guestId } from '@hcengineering/guest'
import {
migrateSpace,
tryMigrate,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
type ModelLogger
} from '@hcengineering/model'
import core from '@hcengineering/model-core'
import { GUEST_DOMAIN } from '.'
export const guestOperation: MigrateOperation = {
async migrate (client: MigrationClient, logger: ModelLogger): Promise<void> {
await tryMigrate(client, guestId, [
{
state: 'migrateRoles',
func: async (client) => {
const stateMap = {
0: AccountRole.User,
1: AccountRole.Maintainer,
2: AccountRole.Owner
}
const createTxes = await client.find<TxCreateDoc<Account>>(DOMAIN_MODEL_TX, {
_class: core.class.TxCreateDoc,
'attributes.role': { $in: [0, 1, 2] }
})
for (const tx of createTxes) {
await client.update(
DOMAIN_MODEL_TX,
{
_id: tx._id
},
{
$set: {
'attributes.role': (stateMap as any)[tx.attributes.role]
}
}
)
}
const updateTxes = await client.find<TxUpdateDoc<Account>>(DOMAIN_MODEL_TX, {
_class: core.class.TxUpdateDoc,
'operations.role': { $in: [0, 1, 2] }
})
for (const tx of updateTxes) {
await client.update(
DOMAIN_MODEL_TX,
{
_id: tx._id
},
{
$set: {
'operations.role': (stateMap as any)[(tx.operations as any).role]
}
}
)
}
}
},
{
state: 'removeDeprecatedSpace',
func: async (client: MigrationClient) => {
await migrateSpace(client, 'guest:space:Links' as Ref<Space>, core.space.Workspace, [GUEST_DOMAIN])
}
}
])
await tryMigrate(client, guestId, [])
},
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {}
}
+1 -4
View File
@@ -1,4 +1,4 @@
import { type Account, type Doc, type Ref } from '@hcengineering/core'
import { type Doc, type Ref } from '@hcengineering/core'
import { guestId } from '@hcengineering/guest'
import guest from '@hcengineering/guest-resources/src/plugin'
import { mergeIds } from '@hcengineering/platform'
@@ -6,9 +6,6 @@ import { type AnyComponent } from '@hcengineering/ui'
import { type Action, type ActionCategory } from '@hcengineering/view'
export default mergeIds(guestId, guest, {
account: {
Guest: '' as Ref<Account>
},
action: {
CreatePublicLink: '' as Ref<Action<Doc, any>>
},
+4 -13
View File
@@ -28,7 +28,6 @@ import {
import {
hrId,
type Department,
type DepartmentMember,
type PublicHoliday,
type Request,
type RequestType,
@@ -53,7 +52,7 @@ import {
import attachment from '@hcengineering/model-attachment'
import calendar from '@hcengineering/model-calendar'
import chunter from '@hcengineering/model-chunter'
import contact, { TEmployee, TPersonAccount } from '@hcengineering/model-contact'
import contact, { TEmployee } from '@hcengineering/model-contact'
import core, { TAttachedDoc, TDoc, TType } from '@hcengineering/model-core'
import view, { classPresenter, createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
@@ -97,8 +96,8 @@ export class TDepartment extends TDoc implements Department {
@Prop(TypeRef(contact.mixin.Employee), hr.string.TeamLead)
teamLead!: Ref<Employee> | null
@Prop(ArrOf(TypeRef(hr.class.DepartmentMember)), contact.string.Members)
members!: Arr<Ref<DepartmentMember>>
@Prop(ArrOf(TypeRef(contact.mixin.Employee)), contact.string.Members)
members!: Arr<Ref<Employee>>
@Prop(ArrOf(TypeRef(contact.class.Contact)), hr.string.Subscribers)
subscribers?: Arr<Ref<Contact>>
@@ -107,10 +106,6 @@ export class TDepartment extends TDoc implements Department {
managers!: Arr<Ref<Employee>>
}
@Model(hr.class.DepartmentMember, contact.class.PersonAccount)
@UX(contact.string.Employee, hr.icon.HR)
export class TDepartmentMember extends TPersonAccount implements DepartmentMember {}
@Mixin(hr.mixin.Staff, contact.mixin.Employee)
@UX(hr.string.Staff, hr.icon.HR, 'STFF', 'name')
export class TStaff extends TEmployee implements Staff {
@@ -191,7 +186,7 @@ export class TPublicHoliday extends TDoc implements PublicHoliday {
}
export function createModel (builder: Builder): void {
builder.createModel(TDepartment, TDepartmentMember, TRequest, TRequestType, TPublicHoliday, TStaff, TTzDate)
builder.createModel(TDepartment, TRequest, TRequestType, TPublicHoliday, TStaff, TTzDate)
builder.createDoc(
workbench.class.Application,
@@ -227,10 +222,6 @@ export function createModel (builder: Builder): void {
editor: hr.component.EditRequest
})
builder.mixin(hr.class.DepartmentMember, core.class.Class, view.mixin.ArrayEditor, {
editor: hr.component.DepartmentStaff
})
classPresenter(builder, hr.class.TzDate, hr.component.TzDatePresenter, hr.component.TzDateEditor)
builder.createDoc(
+32 -2
View File
@@ -13,7 +13,15 @@
// limitations under the License.
//
import { type Space, TxOperations, type Ref } from '@hcengineering/core'
import {
type Space,
TxOperations,
type Ref,
type Class,
type Doc,
DOMAIN_MODEL_TX,
type TxCUD
} from '@hcengineering/core'
import { type Department } from '@hcengineering/hr'
import {
migrateSpace,
@@ -23,7 +31,8 @@ import {
type MigrationClient,
type MigrationUpgradeClient
} from '@hcengineering/model'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import core, { DOMAIN_SPACE, getAccountsFromTxes } from '@hcengineering/model-core'
import hr, { DOMAIN_HR, hrId } from './index'
async function createDepartment (tx: TxOperations): Promise<void> {
@@ -77,6 +86,23 @@ async function migrateDepartments (client: MigrationClient): Promise<void> {
)
}
async function migrateDepartmentMembersToEmployee (client: MigrationClient): Promise<void> {
const departments = await client.find<Department>(DOMAIN_HR, { _class: hr.class.Department })
for (const department of departments) {
const accounts = department.members
if (accounts === undefined || accounts.length === 0) continue
const personAccountsTxes: any[] = await client.find<TxCUD<Doc>>(DOMAIN_MODEL_TX, {
objectClass: 'contact:class:PersonAccount' as Ref<Class<Doc>>,
objectId: { $in: accounts }
})
const personAccounts = getAccountsFromTxes(personAccountsTxes)
await client.update(DOMAIN_HR, { _id: department._id }, { members: personAccounts.map((pAcc: any) => pAcc.person) })
}
}
export const hrOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, hrId, [
@@ -89,6 +115,10 @@ export const hrOperation: MigrateOperation = {
func: async (client: MigrationClient) => {
await migrateSpace(client, 'hr:space:HR' as Ref<Space>, core.space.Workspace, [DOMAIN_HR])
}
},
{
state: 'migrateDepartmentMembersToEmployee',
func: migrateDepartmentMembersToEmployee
}
])
},
+2 -24
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { AccountRole, DOMAIN_MODEL_TX, TxOperations, type Ref, type Status } from '@hcengineering/core'
import { DOMAIN_MODEL_TX, TxOperations, type Ref, type Status } from '@hcengineering/core'
import { leadId, type Lead } from '@hcengineering/lead'
import {
tryMigrate,
@@ -25,7 +25,7 @@ import {
} from '@hcengineering/model'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import contact, { DOMAIN_CONTACT } from '@hcengineering/model-contact'
import { DOMAIN_CONTACT } from '@hcengineering/model-contact'
import task, { createSequence, DOMAIN_TASK, migrateDefaultStatusesBase } from '@hcengineering/model-task'
import lead from './plugin'
@@ -149,24 +149,6 @@ async function migrateDefaultTypeMixins (client: MigrationClient): Promise<void>
)
}
async function migrateDefaultProjectOwners (client: MigrationClient): Promise<void> {
const workspaceOwners = await client.model.findAll(contact.class.PersonAccount, {
role: AccountRole.Owner
})
await client.update(
DOMAIN_SPACE,
{
_id: lead.space.DefaultFunnel
},
{
$set: {
owners: workspaceOwners.map((it) => it._id)
}
}
)
}
export const leadOperation: MigrateOperation = {
async preMigrate (client: MigrationClient, logger: ModelLogger): Promise<void> {
await tryMigrate(client, leadId, [
@@ -188,10 +170,6 @@ export const leadOperation: MigrateOperation = {
await migrateDefaultTypeMixins(client)
}
},
{
state: 'migrateDefaultProjectOwners',
func: migrateDefaultProjectOwners
},
{
state: 'migrate-customer-description',
func: async (client) => {
+2 -2
View File
@@ -15,7 +15,7 @@
import type { Employee } from '@hcengineering/contact'
import {
Account,
PersonId,
IndexKind,
type MarkupBlobRef,
type Role,
@@ -103,7 +103,7 @@ export class TCustomer extends TContact implements Customer {
@Mixin(lead.mixin.DefaultFunnelTypeData, lead.class.Funnel)
@UX(getEmbeddedLabel('Default funnel'), lead.icon.Funnel)
export class TDefaultFunnelTypeData extends TFunnel implements RolesAssignment {
[key: Ref<Role>]: Ref<Account>[]
[key: Ref<Role>]: PersonId[]
}
@Mixin(lead.mixin.LeadTypeData, lead.class.Lead)
+11 -10
View File
@@ -20,7 +20,7 @@ import {
AccountRole,
DOMAIN_MODEL,
IndexKind,
type Account,
type PersonId,
type AttachedDoc,
type Class,
type Collection,
@@ -47,6 +47,7 @@ import {
TypeIntlString,
TypeMarkup,
TypeRef,
TypePersonId,
UX,
type Builder
} from '@hcengineering/model'
@@ -94,12 +95,12 @@ export { notification as default }
@Model(notification.class.BrowserNotification, core.class.Doc, DOMAIN_TRANSIENT)
export class TBrowserNotification extends TDoc implements BrowserNotification {
senderId?: Ref<Account> | undefined
senderId?: PersonId | undefined
tag!: Ref<Doc<Space>>
title!: string
body!: string
onClickLocation?: Location | undefined
user!: Ref<Account>
user!: PersonId
messageId?: Ref<ActivityMessage>
messageClass?: Ref<Class<ActivityMessage>>
objectId!: Ref<Doc>
@@ -108,7 +109,7 @@ export class TBrowserNotification extends TDoc implements BrowserNotification {
@Model(notification.class.PushSubscription, core.class.Doc, DOMAIN_USER_NOTIFY)
export class TPushSubscription extends TDoc implements PushSubscription {
user!: Ref<Account>
user!: PersonId
endpoint!: string
keys!: PushSubscriptionKeys
}
@@ -169,9 +170,9 @@ export class TClassCollaborators extends TClass {
@Mixin(notification.mixin.Collaborators, core.class.Doc)
@UX(notification.string.Collaborators)
export class TCollaborators extends TDoc {
@Prop(ArrOf(TypeRef(core.class.Account)), notification.string.Collaborators)
@Prop(ArrOf(TypePersonId()), notification.string.Collaborators)
@Index(IndexKind.Indexed)
collaborators!: Ref<Account>[]
collaborators!: PersonId[]
}
@Mixin(notification.mixin.NotificationObjectPresenter, core.class.Class)
@@ -191,9 +192,9 @@ export class TNotificationContextPresenter extends TClass implements Notificatio
@Model(notification.class.DocNotifyContext, core.class.Doc, DOMAIN_DOC_NOTIFY)
export class TDocNotifyContext extends TDoc implements DocNotifyContext {
@Prop(TypeRef(core.class.Account), core.string.Account)
@Prop(TypePersonId(), core.string.Account)
@Index(IndexKind.Indexed)
user!: Ref<Account>
user!: PersonId
@Prop(TypeRef(core.class.Doc), core.string.Object)
@Index(IndexKind.Indexed)
@@ -228,9 +229,9 @@ export class TInboxNotification extends TDoc implements InboxNotification {
@Index(IndexKind.Indexed)
docNotifyContext!: Ref<DocNotifyContext>
@Prop(TypeRef(core.class.Account), core.string.Account)
@Prop(TypePersonId(), core.string.Account)
@Index(IndexKind.Indexed)
user!: Ref<Account>
user!: PersonId
@Prop(TypeBoolean(), core.string.Boolean)
// @Index(IndexKind.Indexed)
+174 -4
View File
@@ -15,9 +15,19 @@
import chunter from '@hcengineering/chunter'
import contact, { type PersonSpace } from '@hcengineering/contact'
import core, { DOMAIN_TX, type Class, type Doc, type DocumentQuery, type Ref, type Space } from '@hcengineering/core'
import core, {
DOMAIN_TX,
MeasureMetricsContext,
type Class,
type Doc,
type DocumentQuery,
type Ref,
type Space
} from '@hcengineering/core'
import {
migrateSpace,
type MigrateUpdate,
type MigrationDocumentQuery,
tryMigrate,
type MigrateOperation,
type MigrationClient,
@@ -31,7 +41,7 @@ import notification, {
} from '@hcengineering/notification'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
import { DOMAIN_SPACE, getSocialIdByOldAccount } from '@hcengineering/model-core'
import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION, DOMAIN_USER_NOTIFY } from './index'
export async function removeNotifications (
@@ -221,6 +231,162 @@ export async function migrateDuplicateContexts (client: MigrationClient): Promis
}
}
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('notification migrateAccountsToSocialIds', {})
const hierarchy = client.hierarchy
const socialIdByAccount = await getSocialIdByOldAccount(client)
ctx.info('processing collaborators ', {})
for (const domain of client.hierarchy.domains()) {
ctx.info('processing domain ', { domain })
let processed = 0
const iterator = await client.traverse(domain, {})
try {
while (true) {
const docs = await iterator.next(200)
if (docs === null || docs.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
for (const doc of docs) {
const mixin = hierarchy.as(doc, notification.mixin.Collaborators)
const oldCollaborators = mixin.collaborators
if (oldCollaborators === undefined || oldCollaborators.length === 0) continue
const newCollaborators = oldCollaborators.map((c) => socialIdByAccount[c] ?? c)
operations.push({
filter: { _id: doc._id },
update: {
[`${notification.mixin.Collaborators}`]: {
collaborators: newCollaborators
}
}
})
}
if (operations.length > 0) {
await client.bulk(domain, operations)
}
processed += docs.length
ctx.info('...processed', { count: processed })
}
ctx.info('finished processing domain ', { domain, processed })
} finally {
await iterator.close()
}
}
ctx.info('finished processing collaborators ', {})
ctx.info('processing notifications fields ', {})
function chunkArray<T> (array: T[], chunkSize: number): T[][] {
const chunks: T[][] = []
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize))
}
return chunks
}
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
for (const [accId, socialId] of Object.entries(socialIdByAccount)) {
if (accId === socialId) continue
operations.push({
filter: {
user: accId,
_class: {
$in: [
notification.class.DocNotifyContext,
notification.class.BrowserNotification,
notification.class.PushSubscription,
notification.class.InboxNotification,
notification.class.ActivityInboxNotification,
notification.class.CommonInboxNotification
]
}
},
update: {
user: socialId
}
})
operations.push({
filter: {
senderId: accId,
_class: notification.class.BrowserNotification
},
update: {
senderId: socialId
}
})
}
if (operations.length > 0) {
const operationsChunks = chunkArray(operations, 40)
let processed = 0
for (const operationsChunk of operationsChunks) {
if (operationsChunk.length === 0) continue
await client.bulk(DOMAIN_NOTIFICATION, operationsChunk)
processed++
if (operationsChunks.length > 1) {
ctx.info('processed chunk', { processed, of: operationsChunks.length })
}
}
} else {
ctx.info('no user accounts to migrate')
}
ctx.info('finished processing notifications fields ', {})
ctx.info('processing doc notify contexts ', {})
const dncIterator = await client.traverse<DocNotifyContext>(DOMAIN_DOC_NOTIFY, {
_class: notification.class.DocNotifyContext
})
try {
let processed = 0
while (true) {
const docs = await dncIterator.next(200)
if (docs === null || docs.length === 0) {
break
}
const operations: {
filter: MigrationDocumentQuery<DocNotifyContext>
update: MigrateUpdate<DocNotifyContext>
}[] = []
for (const doc of docs) {
const oldUser = doc.user
const newUser = socialIdByAccount[oldUser] ?? oldUser
if (newUser !== oldUser) {
operations.push({
filter: { _id: doc._id },
update: {
user: newUser
}
})
}
}
if (operations.length > 0) {
await client.bulk(DOMAIN_DOC_NOTIFY, operations)
}
processed += docs.length
ctx.info('...processed', { count: processed })
}
} finally {
await dncIterator.close()
}
ctx.info('finished processing doc notify contexts ', {})
}
export async function migrateSettings (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_PREFERENCE,
@@ -467,9 +633,13 @@ export const notificationOperation: MigrateOperation = {
await client.update(DOMAIN_DOC_NOTIFY, { space: core.space.Space }, { space: core.space.Workspace })
}
},
// {
// state: 'migrate-notifications-object',
// func: migrateNotificationsObject
// },
{
state: 'migrate-notifications-object',
func: migrateNotificationsObject
state: 'accounts-to-social-ids',
func: migrateAccountsToSocialIds
}
])
},
+6 -5
View File
@@ -22,8 +22,8 @@ import { type Attachment } from '@hcengineering/attachment'
import contact from '@hcengineering/contact'
import chunter from '@hcengineering/chunter'
import { getRoleAttributeProps } from '@hcengineering/setting'
import type { Type, CollectionSize, Markup, Arr, RolesAssignment, Permission, Role } from '@hcengineering/core'
import { IndexKind, Ref, Account } from '@hcengineering/core'
import type { Type, Ref, CollectionSize, Markup, Arr, RolesAssignment, Permission, Role } from '@hcengineering/core'
import { IndexKind, PersonId } from '@hcengineering/core'
import {
type Builder,
Model,
@@ -39,6 +39,7 @@ import {
ArrOf,
TypeAny,
ReadOnly,
TypePersonId,
Mixin
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
@@ -78,8 +79,8 @@ export class TTypeProductVersionState extends TType {}
@Model(products.class.Product, documents.class.ExternalSpace)
@UX(products.string.Product, products.icon.Product, 'Product', 'name', undefined, products.string.Products)
export class TProduct extends TExternalSpace implements Product {
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Members)
declare members: Arr<Ref<Account>>
@Prop(ArrOf(TypePersonId()), core.string.Members)
declare members: Arr<PersonId>
@Prop(TypeMarkup(), products.string.Description)
@Index(IndexKind.FullText)
@@ -145,7 +146,7 @@ export class TProductVersion extends TProject implements ProductVersion {
@Mixin(products.mixin.ProductTypeData, products.class.Product)
@UX(getEmbeddedLabel('Default Products'), products.icon.ProductVersion)
export class TProductTypeData extends TProduct implements RolesAssignment {
[key: Ref<Role>]: Ref<Account>[]
[key: Ref<Role>]: PersonId[]
}
function defineProduct (builder: Builder): void {
+2 -2
View File
@@ -15,7 +15,7 @@
import type { Employee, Organization } from '@hcengineering/contact'
import {
Account,
PersonId,
IndexKind,
type Collection,
type MarkupBlobRef,
@@ -251,7 +251,7 @@ export class TOpinion extends TAttachedDoc implements Opinion {
@Mixin(recruit.mixin.DefaultVacancyTypeData, recruit.class.Vacancy)
@UX(getEmbeddedLabel('Default vacancy'), recruit.icon.Vacancy)
export class TDefaultVacancyTypeData extends TVacancy implements RolesAssignment {
[key: Ref<Role>]: Ref<Account>[]
[key: Ref<Role>]: PersonId[]
}
@Mixin(recruit.mixin.ApplicantTypeData, recruit.class.Applicant)
+2 -72
View File
@@ -12,88 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import contact, { type Person, type PersonAccount } from '@hcengineering/contact'
import core, { DOMAIN_MODEL_TX, type Ref, type TxCreateDoc } from '@hcengineering/core'
import { requestId } from '@hcengineering/request'
import {
tryMigrate,
type MigrateOperation,
type MigrateUpdate,
type MigrationClient,
type MigrationDocumentQuery,
type MigrationUpgradeClient,
type ModelLogger
} from '@hcengineering/model'
import request, { requestId, type Request } from '@hcengineering/request'
import { DOMAIN_REQUEST } from '.'
async function migrateRequestPersonAccounts (client: MigrationClient): Promise<void> {
const descendants = client.hierarchy.getDescendants(request.class.Request)
const requests = await client.find<Request>(DOMAIN_REQUEST, {
_class: { $in: descendants }
})
const personAccountsCreateTxes = await client.find(DOMAIN_MODEL_TX, {
_class: core.class.TxCreateDoc,
objectClass: contact.class.PersonAccount
})
const personAccountToPersonMap = personAccountsCreateTxes.reduce<Record<Ref<PersonAccount>, Ref<Person>>>(
(map, tx) => {
const ctx = tx as TxCreateDoc<PersonAccount>
map[ctx.objectId] = ctx.attributes.person
return map
},
{}
)
const operations: { filter: MigrationDocumentQuery<Request>, update: MigrateUpdate<Request> }[] = []
for (const request of requests) {
const newRequestedPersons = request.requested
.map((paId) => personAccountToPersonMap[paId as unknown as Ref<PersonAccount>])
.filter((p) => p != null)
const newApprovedPersons = request.approved
.map((paId) => personAccountToPersonMap[paId as unknown as Ref<PersonAccount>])
.filter((p) => p != null)
const newRejectedPerson =
request.rejected != null ? personAccountToPersonMap[request.rejected as unknown as Ref<PersonAccount>] : undefined
if (newRequestedPersons.length > 0) {
operations.push({
filter: {
_id: request._id
},
update: {
requested: newRequestedPersons,
approved: newApprovedPersons
}
})
}
if (newRejectedPerson !== undefined) {
operations.push({
filter: {
_id: request._id
},
update: {
rejected: newRejectedPerson
}
})
}
}
if (operations.length > 0) {
await client.bulk(DOMAIN_REQUEST, operations)
}
}
export const requestOperation: MigrateOperation = {
async migrate (client: MigrationClient, logger: ModelLogger): Promise<void> {
await tryMigrate(client, requestId, [
{
state: 'migrateRequestPersonAccounts',
func: migrateRequestPersonAccounts
}
])
await tryMigrate(client, requestId, [])
},
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {}
}
+1 -1
View File
@@ -54,7 +54,7 @@ function getActivityControl (client: MigrationClient): ActivityControl {
findAll: async (ctx, _class, query, options) =>
toFindResult(await client.find(client.hierarchy.getDomain(_class), query, options)),
storageAdapter: client.storageAdapter,
workspace: client.workspaceId
workspace: client.wsIds
}
}
-1
View File
@@ -25,7 +25,6 @@ import { TChatMessage } from '@hcengineering/model-chunter'
export { serverAiBotId } from '@hcengineering/server-ai-bot'
export const DOMAIN_AI_BOT = 'ai_bot' as Domain
@Mixin(aiBot.mixin.TransferredMessage, chunter.class.ChatMessage)
export class TTransferredMessage extends TChatMessage implements TransferredMessage {
messageId!: Ref<ChatMessage>
+10 -2
View File
@@ -34,10 +34,18 @@ export function createModel (builder: Builder): void {
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverCalendar.trigger.OnPersonAccountCreate,
trigger: serverCalendar.trigger.OnSocialIdentityCreate,
txMatch: {
_class: core.class.TxCreateDoc,
objectClass: contact.class.PersonAccount
objectClass: contact.class.SocialIdentity
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverCalendar.trigger.OnEmployee,
txMatch: {
_class: core.class.TxMixin,
mixin: contact.mixin.Employee
}
})
+8 -8
View File
@@ -71,6 +71,14 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverContact.trigger.OnSocialIdentityCreate,
txMatch: {
_class: core.class.TxCreateDoc,
objectClass: contact.class.SocialIdentity
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverContact.trigger.OnEmployeeCreate,
txMatch: {
@@ -81,14 +89,6 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverContact.trigger.OnPersonAccountCreate,
txMatch: {
objectClass: contact.class.PersonAccount,
_class: core.class.TxCreateDoc
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverContact.trigger.OnSpaceTypeMembers,
txMatch: {
@@ -14,6 +14,14 @@ import notification from '@hcengineering/notification'
export { serverDocumentsId } from '@hcengineering/server-controlled-documents/src/index'
export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverDocuments.trigger.OnSocialIdentityCreate,
txMatch: {
_class: core.class.TxCreateDoc,
objectClass: contact.class.SocialIdentity
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverDocuments.trigger.OnDocDeleted,
txMatch: {
@@ -50,13 +58,6 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverDocuments.trigger.OnWorkspaceOwnerAdded,
txMatch: {
objectClass: contact.class.PersonAccount
}
})
builder.mixin(documents.class.DocumentMeta, core.class.Class, serverCore.mixin.SearchPresenter, {
iconConfig: {
component: documents.component.DocumentIcon
+3 -2
View File
@@ -44,9 +44,10 @@ export function createModel (builder: Builder): void {
)
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverLead.trigger.OnWorkspaceOwnerAdded,
trigger: serverLead.trigger.OnSocialIdentityCreate,
txMatch: {
objectClass: contact.class.PersonAccount
_class: core.class.TxCreateDoc,
objectClass: contact.class.SocialIdentity
}
})
}
+8 -7
View File
@@ -51,6 +51,14 @@ export function createModel (builder: Builder): void {
title: [['title']]
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTracker.trigger.OnSocialIdentityCreate,
txMatch: {
_class: core.class.TxCreateDoc,
objectClass: contact.class.SocialIdentity
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTracker.trigger.OnIssueUpdate,
txMatch: {
@@ -66,13 +74,6 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTracker.trigger.OnWorkspaceOwnerAdded,
txMatch: {
objectClass: contact.class.PersonAccount
}
})
builder.mixin(
tracker.ids.AssigneeNotification,
notification.class.NotificationType,
+3 -3
View File
@@ -15,8 +15,8 @@
import activity from '@hcengineering/activity'
import contact from '@hcengineering/contact'
import { AccountRole, DOMAIN_MODEL, type Account, type Blob, type Domain, type Ref } from '@hcengineering/core'
import { Mixin, Model, UX, type Builder } from '@hcengineering/model'
import { AccountRole, DOMAIN_MODEL, type PersonId, type Blob, type Domain, type Ref } from '@hcengineering/core'
import { Mixin, Model, type Builder, UX } from '@hcengineering/model'
import core, { TClass, TConfiguration, TDoc } from '@hcengineering/model-core'
import view, { createAction } from '@hcengineering/model-view'
import notification from '@hcengineering/notification'
@@ -53,7 +53,7 @@ export class TIntegration extends TDoc implements Integration {
type!: Ref<IntegrationType>
disabled!: boolean
value!: string
shared!: Ref<Account>[]
shared!: PersonId[]
error?: IntlString | null
}
@Model(setting.class.SettingsCategory, core.class.Doc, DOMAIN_MODEL)
+55 -2
View File
@@ -13,17 +13,66 @@
// limitations under the License.
//
import core, { type Ref, type Space } from '@hcengineering/core'
import core, { MeasureMetricsContext, type Ref, type Space } from '@hcengineering/core'
import {
migrateSpace,
type MigrateUpdate,
type MigrationDocumentQuery,
tryMigrate,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient
} from '@hcengineering/model'
import { settingId } from '@hcengineering/setting'
import setting, { type Integration, settingId } from '@hcengineering/setting'
import { getSocialIdByOldAccount } from '@hcengineering/model-core'
import { DOMAIN_SETTING } from '.'
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('setting migrateAccountsToSocialIds', {})
const socialIdByAccount = await getSocialIdByOldAccount(client)
ctx.info('processing setting integration shared ', {})
const iterator = await client.traverse(DOMAIN_SETTING, { _class: setting.class.Integration })
try {
let processed = 0
while (true) {
const docs = await iterator.next(200)
if (docs === null || docs.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<Integration>, update: MigrateUpdate<Integration> }[] = []
for (const doc of docs) {
const integration = doc as Integration
if (integration.shared === undefined || integration.shared.length === 0) continue
const newShared = integration.shared.map((s) => socialIdByAccount[s] ?? s)
operations.push({
filter: { _id: integration._id },
update: {
shared: newShared
}
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_SETTING, operations)
}
processed += docs.length
ctx.info('...processed', { count: processed })
}
} finally {
await iterator.close()
}
ctx.info('finished processing setting integration shared ', {})
}
export const settingOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, settingId, [
@@ -32,6 +81,10 @@ export const settingOperation: MigrateOperation = {
func: async (client: MigrationClient) => {
await migrateSpace(client, 'setting:space:Setting' as Ref<Space>, core.space.Workspace, [DOMAIN_SETTING])
}
},
{
state: 'accounts-to-social-ids',
func: migrateAccountsToSocialIds
}
])
},
-17
View File
@@ -371,23 +371,6 @@ export function createModel (builder: Builder): void {
task.action.Move
)
createAction(
builder,
{
label: task.string.Export,
action: task.actionImpl.ExportTasks,
icon: view.icon.Move,
input: 'selection',
category: view.category.General,
target: task.class.Task,
context: {
mode: ['context', 'browser'],
group: 'tools'
}
},
task.action.ExportTasks
)
builder.createDoc(
core.class.StatusCategory,
core.space.Model,
+2 -4
View File
@@ -27,13 +27,11 @@ export default mergeIds(taskId, task, {
action: {
EditStatuses: '' as Ref<Action>,
ArchiveState: '' as Ref<Action>,
PublicLink: '' as Ref<Action<Doc, any>>,
ExportTasks: '' as Ref<Action>
PublicLink: '' as Ref<Action<Doc, any>>
},
actionImpl: {
EditStatuses: '' as ViewAction,
SelectStatus: '' as ViewAction,
ExportTasks: '' as ViewAction
SelectStatus: '' as ViewAction
},
category: {
Task: '' as Ref<ActionCategory>,
+4 -4
View File
@@ -32,18 +32,18 @@ import contact from '@hcengineering/contact'
import chunter from '@hcengineering/chunter'
import { getEmbeddedLabel } from '@hcengineering/platform'
import {
Account,
DateRangeMode,
IndexKind,
type RolesAssignment,
type Role,
Ref,
type Ref,
type Domain,
type Timestamp,
type Type,
type CollectionSize,
type MarkupBlobRef,
type Class
type Class,
type PersonId
} from '@hcengineering/core'
import {
Mixin,
@@ -107,7 +107,7 @@ export class TTestProject extends TTypedSpace implements TestProject {
@Mixin(testManagement.mixin.DefaultProjectTypeData, testManagement.class.TestProject)
@UX(getEmbeddedLabel('Default project'), testManagement.icon.TestProject)
export class TDefaultProjectTypeData extends TTestProject implements RolesAssignment {
[key: Ref<Role>]: Ref<Account>[]
[key: Ref<Role>]: PersonId[]
}
/**
+2 -126
View File
@@ -13,8 +13,7 @@
// limitations under the License.
//
import { type PersonAccount } from '@hcengineering/contact'
import { type Account, type Doc, type Ref, SortingOrder, TxOperations } from '@hcengineering/core'
import { TxOperations } from '@hcengineering/core'
import {
type MigrateOperation,
type MigrationClient,
@@ -24,132 +23,12 @@ import {
tryUpgrade,
createDefaultSpace
} from '@hcengineering/model'
import { makeRank } from '@hcengineering/rank'
import core from '@hcengineering/model-core'
import task from '@hcengineering/task'
import tags from '@hcengineering/tags'
import { timeId, type ToDo, ToDoPriority } from '@hcengineering/time'
import { timeId, ToDoPriority } from '@hcengineering/time'
import { DOMAIN_TIME } from '.'
import time from './plugin'
export async function migrateWorkSlots (client: TxOperations): Promise<void> {
const h = client.getHierarchy()
const desc = h.getDescendants(task.class.Task)
const oldWorkSlots = await client.findAll(time.class.WorkSlot, {
attachedToClass: { $in: desc }
})
const now = Date.now()
const todos = new Map<Ref<Doc>, Ref<ToDo>>()
const count = new Map<Ref<ToDo>, number>()
let rank = makeRank(undefined, undefined)
for (const oldWorkSlot of oldWorkSlots) {
const todo = todos.get(oldWorkSlot.attachedTo)
if (todo === undefined) {
const acc = oldWorkSlot.space.replace('_calendar', '') as Ref<Account>
const account = (await client.findOne(core.class.Account, { _id: acc })) as PersonAccount
if (account.person !== undefined) {
rank = makeRank(undefined, rank)
const todo = await client.addCollection(
time.class.ProjectToDo,
time.space.ToDos,
oldWorkSlot.attachedTo,
oldWorkSlot.attachedToClass,
'todos',
{
attachedSpace: (oldWorkSlot as any).attachedSpace,
title: oldWorkSlot.title,
description: '',
doneOn: oldWorkSlot.dueDate > now ? null : oldWorkSlot.dueDate,
workslots: 0,
priority: ToDoPriority.NoPriority,
user: account.person,
visibility: 'public',
rank
}
)
await client.update(oldWorkSlot, {
attachedTo: todo,
attachedToClass: time.class.ProjectToDo,
collection: 'workslots'
})
todos.set(oldWorkSlot.attachedTo, todo)
count.set(todo, 1)
}
} else {
await client.update(oldWorkSlot, {
attachedTo: todo,
attachedToClass: time.class.ProjectToDo,
collection: 'workslots'
})
const c = count.get(todo) ?? 1
count.set(todo, c + 1)
}
}
for (const [todoId, c] of count.entries()) {
const todo = await client.findOne(time.class.ToDo, { _id: todoId })
if (todo === undefined) continue
const tx = client.txFactory.createTxUpdateDoc(time.class.ToDo, todo.space, todo._id, {
workslots: c
})
tx.space = core.space.DerivedTx
await client.tx(tx)
}
}
async function migrateTodosSpace (client: TxOperations): Promise<void> {
const oldTodos = await client.findAll(time.class.ToDo, {
space: { $ne: time.space.ToDos }
})
for (const oldTodo of oldTodos) {
const account = (await client.findOne(core.class.Account, {
_id: oldTodo.space as string as Ref<Account>
})) as PersonAccount
if (account.person === undefined) continue
await client.update(oldTodo, {
user: account.person,
space: time.space.ToDos
})
}
}
async function migrateTodosRanks (client: TxOperations): Promise<void> {
const doneTodos = await client.findAll(
time.class.ToDo,
{
rank: { $exists: false },
doneOn: null
},
{
sort: { modifiedOn: SortingOrder.Ascending }
}
)
let doneTodoRank = makeRank(undefined, undefined)
for (const todo of doneTodos) {
await client.update(todo, {
rank: doneTodoRank
})
doneTodoRank = makeRank(undefined, doneTodoRank)
}
const undoneTodos = await client.findAll(
time.class.ToDo,
{
rank: { $exists: false },
doneOn: { $ne: null }
},
{
sort: { doneOn: SortingOrder.Ascending }
}
)
let undoneTodoRank = makeRank(undefined, undefined)
for (const todo of undoneTodos) {
await client.update(todo, {
rank: undoneTodoRank
})
undoneTodoRank = makeRank(undefined, undoneTodoRank)
}
}
async function fillProps (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_TIME,
@@ -200,9 +79,6 @@ export const timeOperation: MigrateOperation = {
},
time.category.Other
)
await migrateWorkSlots(tx)
await migrateTodosSpace(tx)
await migrateTodosRanks(tx)
}
}
])
-24
View File
@@ -15,7 +15,6 @@
import activity, { type DocUpdateMessage } from '@hcengineering/activity'
import core, {
AccountRole,
DOMAIN_MODEL_TX,
DOMAIN_STATUS,
type Ref,
@@ -47,7 +46,6 @@ import tracker, {
trackerId
} from '@hcengineering/tracker'
import contact from '@hcengineering/model-contact'
import { classicIssueTaskStatuses } from '.'
async function createDefaultProject (tx: TxOperations): Promise<void> {
@@ -335,24 +333,6 @@ async function migrateDefaultTypeMixins (client: MigrationClient): Promise<void>
)
}
async function migrateDefaultProjectOwners (client: MigrationClient): Promise<void> {
const workspaceOwners = await client.model.findAll(contact.class.PersonAccount, {
role: AccountRole.Owner
})
await client.update(
DOMAIN_SPACE,
{
_id: tracker.project.DefaultProject
},
{
$set: {
owners: workspaceOwners.map((it) => it._id)
}
}
)
}
async function migrateIssueStatuses (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_MODEL_TX,
@@ -424,10 +404,6 @@ export const trackerOperation: MigrateOperation = {
{
state: 'migrateDefaultTypeMixins',
func: migrateDefaultTypeMixins
},
{
state: 'migrateDefaultProjectOwners',
func: migrateDefaultProjectOwners
}
])
},
+2 -2
View File
@@ -29,7 +29,7 @@ import {
type RolesAssignment,
type Role,
type CollectionSize,
Account
PersonId
} from '@hcengineering/core'
import {
ArrOf,
@@ -422,7 +422,7 @@ export class TProjectTargetPreference extends TPreference implements ProjectTarg
@Mixin(tracker.mixin.ClassicProjectTypeData, tracker.class.Project)
@UX(getEmbeddedLabel('Classic project'), tracker.icon.Issues)
export class TClassicProjectTypeData extends TProject implements RolesAssignment {
[key: Ref<Role>]: Ref<Account>[]
[key: Ref<Role>]: PersonId[]
}
@Mixin(tracker.mixin.IssueTypeData, tracker.class.Issue)
+2 -2
View File
@@ -37,7 +37,7 @@ import core, {
type TypedSpace,
RolesAssignment,
Role,
Account
PersonId
} from '@hcengineering/core'
import {
ArrOf,
@@ -262,5 +262,5 @@ export class TTrainingAttempt extends TAttachedDoc implements TrainingAttempt {
@Mixin(training.mixin.TrainingsTypeData, core.class.TypedSpace)
@UX(getEmbeddedLabel('Default Trainings'), training.icon.TrainingApplication)
export class TTrainingsTypeData extends TTypedSpace implements RolesAssignment {
[key: Ref<Role>]: Ref<Account>[]
[key: Ref<Role>]: PersonId[]
}
+18 -2
View File
@@ -14,7 +14,7 @@
//
import {
type Account,
type PersonId,
type Class,
type Client,
DOMAIN_MODEL,
@@ -116,7 +116,7 @@ export class TFilteredView extends TDoc implements FilteredView {
viewOptions?: ViewOptions
filterClass?: Ref<Class<Doc>>
viewletId?: Ref<Viewlet> | null
users!: Ref<Account>[]
users!: PersonId[]
attachedTo!: string
sharable?: boolean
}
@@ -936,6 +936,10 @@ export function createModel (builder: Builder): void {
group: 'bottom'
})
builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.AttributeFilter, {
component: view.component.ValueFilter
})
builder.createDoc(
view.class.FilterMode,
core.space.Model,
@@ -1172,6 +1176,10 @@ export function createModel (builder: Builder): void {
presenter: view.component.StringFilterPresenter
})
builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.AttributeFilterPresenter, {
presenter: view.component.StringFilterPresenter
})
classPresenter(builder, core.class.EnumOf, view.component.EnumPresenter, view.component.EnumEditor)
createAction(
@@ -1216,6 +1224,14 @@ export function createModel (builder: Builder): void {
indexes: [],
searchDisabled: true
})
builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.AttributePresenter, {
presenter: view.component.PersonIdPresenter
})
builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.AttributeFilterPresenter, {
presenter: view.component.PersonIdFilterValuePresenter
})
}
export default view
+54
View File
@@ -15,13 +15,18 @@
import {
type MigrateOperation,
type MigrateUpdate,
type MigrationClient,
type MigrationDocumentQuery,
type MigrationUpgradeClient,
tryMigrate
} from '@hcengineering/model'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import view, { type Filter, type FilteredView, type ViewletPreference, viewId } from '@hcengineering/view'
import { getSocialIdByOldAccount } from '@hcengineering/model-core'
import { DOMAIN_VIEW } from '.'
import { MeasureMetricsContext } from '@hcengineering/core'
async function removeDoneStatePref (client: MigrationClient): Promise<void> {
const prefs = await client.find<ViewletPreference>(DOMAIN_PREFERENCE, {
@@ -76,6 +81,51 @@ async function removeDoneStateFilter (client: MigrationClient): Promise<void> {
}
}
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('view migrateAccountsToSocialIds', {})
const socialIdByAccount = await getSocialIdByOldAccount(client)
ctx.info('processing view filtered view users ', {})
const iterator = await client.traverse(DOMAIN_VIEW, { _class: view.class.FilteredView })
try {
let processed = 0
while (true) {
const docs = await iterator.next(200)
if (docs === null || docs.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<FilteredView>, update: MigrateUpdate<FilteredView> }[] = []
for (const doc of docs) {
const filteredView = doc as FilteredView
if (filteredView.users === undefined || filteredView.users.length === 0) continue
const newUsers = filteredView.users.map((u) => socialIdByAccount[u] ?? u)
operations.push({
filter: { _id: filteredView._id },
update: {
users: newUsers
}
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_VIEW, operations)
}
processed += docs.length
ctx.info('...processed', { count: processed })
}
} finally {
await iterator.close()
}
ctx.info('finished processing view filtered view users ', {})
}
export const viewOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, viewId, [
@@ -86,6 +136,10 @@ export const viewOperation: MigrateOperation = {
{
state: 'remove-done-state-filter',
func: removeDoneStateFilter
},
{
state: 'accounts-to-social-ids',
func: migrateAccountsToSocialIds
}
])
},
+1
View File
@@ -89,6 +89,7 @@ export default mergeIds(viewId, view, {
EnumPresenter: '' as AnyComponent,
StatusPresenter: '' as AnyComponent,
StatusRefPresenter: '' as AnyComponent,
PersonIdFilterValuePresenter: '' as AnyComponent,
DateFilterPresenter: '' as AnyComponent,
StringFilterPresenter: '' as AnyComponent,
AudioViewer: '' as AnyComponent,
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+50
View File
@@ -0,0 +1,50 @@
{
"name": "@hcengineering/account-client",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"files": [
"lib/**/*",
"types/**/*",
"tsconfig.json"
],
"author": "Hardcore Engineering Inc.",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"test": "jest --passWithNoTests --silent",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"cross-env": "~7.0.3",
"@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",
"esbuild": "^0.24.2",
"@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"
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
"@hcengineering/platform": "^0.6.11"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
+562
View File
@@ -0,0 +1,562 @@
//
// 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 AccountRole,
BackupStatus,
Data,
type Person,
PersonUuid,
SocialId,
Version,
type WorkspaceInfoWithStatus,
type WorkspaceMemberInfo,
WorkspaceMode,
concatLink
} from '@hcengineering/core'
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
import type { LoginInfo, OtpInfo, WorkspaceLoginInfo, RegionInfo, WorkspaceOperation } from './types'
/** @public */
export interface AccountClient {
// Static methods
getProviders: () => Promise<string[]>
// RPC
getUserWorkspaces: () => Promise<WorkspaceInfoWithStatus[]>
selectWorkspace: (
workspaceUrl: string,
kind?: 'external' | 'internal' | 'byregion',
externalRegions?: string[]
) => Promise<WorkspaceLoginInfo>
validateOtp: (email: string, code: string) => Promise<LoginInfo>
loginOtp: (email: string) => Promise<OtpInfo>
getLoginInfoByToken: () => Promise<LoginInfo | WorkspaceLoginInfo>
restorePassword: (password: string) => Promise<LoginInfo>
confirm: () => Promise<LoginInfo>
requestPasswordReset: (email: string) => Promise<void>
sendInvite: (email: string, role?: AccountRole) => Promise<void>
leaveWorkspace: (account: string) => Promise<LoginInfo | null>
changeUsername: (first: string, last: string) => Promise<void>
changePassword: (oldPassword: string, newPassword: string) => Promise<void>
signUpJoin: (
email: string,
password: string,
first: string,
last: string,
inviteId: string
) => Promise<WorkspaceLoginInfo>
join: (email: string, password: string, inviteId: string) => Promise<WorkspaceLoginInfo>
createInviteLink: (
exp: number,
emailMask: string,
limit: number,
role: AccountRole,
personId?: any
) => Promise<string>
checkJoin: (inviteId: string) => Promise<WorkspaceLoginInfo>
getWorkspaceInfo: (updateLastVisit?: boolean) => Promise<WorkspaceInfoWithStatus>
getRegionInfo: () => Promise<RegionInfo[]>
createWorkspace: (name: string, region?: string) => Promise<WorkspaceLoginInfo>
signUpOtp: (email: string, first: string, last: string) => Promise<OtpInfo>
signUp: (email: string, password: string, first: string, last: string) => Promise<LoginInfo>
login: (email: string, password: string) => Promise<LoginInfo>
getPerson: () => Promise<Person>
getSocialIds: () => Promise<SocialId[]>
getWorkspaceMembers: () => Promise<WorkspaceMemberInfo[]>
updateWorkspaceRole: (account: string, role: AccountRole) => Promise<void>
updateWorkspaceName: (name: string) => Promise<void>
deleteWorkspace: () => Promise<void>
findPerson: (socialString: string) => Promise<PersonUuid | undefined>
// Service methods
workerHandshake: (region: string, version: Data<Version>, operation: WorkspaceOperation) => Promise<void>
getPendingWorkspace: (
region: string,
version: Data<Version>,
operation: WorkspaceOperation
) => Promise<WorkspaceInfoWithStatus | null>
updateWorkspaceInfo: (
wsUuid: string,
event: string,
version: Data<Version>,
progress: number,
message?: string
) => Promise<void>
listWorkspaces: (region?: string | null, mode?: WorkspaceMode | null) => Promise<WorkspaceInfoWithStatus[]>
performWorkspaceOperation: (
workspaceId: string | string[],
event: 'archive' | 'migrate-to' | 'unarchive' | 'delete',
...params: any
) => Promise<boolean>
assignWorkspace: (email: string, workspaceUuid: string, role: AccountRole) => Promise<void>
updateBackupInfo: (info: BackupStatus) => Promise<void>
updateWorkspaceRoleBySocialId: (socialKey: string, targetRole: AccountRole) => Promise<void>
}
/** @public */
export function getClient (accountsUrl?: string, token?: string): AccountClient {
if (accountsUrl === undefined) {
throw new Error('Accounts url not specified')
}
return new AccountClientImpl(accountsUrl, token)
}
interface Request {
method: string
params: any[]
}
class AccountClientImpl implements AccountClient {
constructor (
private readonly url: string,
private readonly token?: string
) {
if (url === '') {
throw new Error('Accounts url not specified')
}
}
async getProviders (): Promise<string[]> {
return await retry(5, async () => {
const response = await fetch(concatLink(this.url, '/providers'))
return await response.json()
})
}
private async rpc<T>(request: Request): Promise<T> {
const response = await fetch(this.url, {
method: 'POST',
keepalive: true,
headers: {
...(this.token === undefined
? {}
: {
Authorization: 'Bearer ' + this.token
}),
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
})
const result = await response.json()
if (result.error != null) {
throw new PlatformError(result.error)
}
return result.result
}
private flattenStatus (ws: any): WorkspaceInfoWithStatus {
if (ws === undefined) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, {}))
}
const status = ws.status
if (status === undefined) {
return ws
}
const result = { ...ws, ...status }
delete result.status
return result
}
async getUserWorkspaces (): Promise<WorkspaceInfoWithStatus[]> {
const request = {
method: 'getUserWorkspaces' as const,
params: []
}
return (await this.rpc<any[]>(request)).map((ws) => this.flattenStatus(ws))
}
async selectWorkspace (
workspaceUrl: string,
kind: 'external' | 'internal' | 'byregion' = 'external',
externalRegions: string[] = []
): Promise<WorkspaceLoginInfo> {
const request = {
method: 'selectWorkspace' as const,
params: [workspaceUrl, kind, externalRegions]
}
return await this.rpc(request)
}
async validateOtp (email: string, code: string): Promise<LoginInfo> {
const request = {
method: 'validateOtp' as const,
params: [email, code]
}
return await this.rpc(request)
}
async loginOtp (email: string): Promise<OtpInfo> {
const request = {
method: 'loginOtp' as const,
params: [email]
}
return await this.rpc(request)
}
async getLoginInfoByToken (): Promise<LoginInfo | WorkspaceLoginInfo> {
const request = {
method: 'getLoginInfoByToken' as const,
params: []
}
return await this.rpc(request)
}
async restorePassword (password: string): Promise<LoginInfo> {
const request = {
method: 'restorePassword' as const,
params: [password]
}
return await this.rpc(request)
}
async confirm (): Promise<LoginInfo> {
const request = {
method: 'confirm' as const,
params: []
}
return await this.rpc(request)
}
async requestPasswordReset (email: string): Promise<void> {
const request = {
method: 'requestPasswordReset' as const,
params: [email]
}
await this.rpc(request)
}
async sendInvite (email: string, role?: AccountRole): Promise<void> {
const request = {
method: 'sendInvite' as const,
params: [email, role]
}
await this.rpc(request)
}
async leaveWorkspace (account: string): Promise<LoginInfo | null> {
const request = {
method: 'leaveWorkspace' as const,
params: [account]
}
return await this.rpc(request)
}
async changeUsername (first: string, last: string): Promise<void> {
const request = {
method: 'changeUsername' as const,
params: [first, last]
}
await this.rpc(request)
}
async changePassword (oldPassword: string, newPassword: string): Promise<void> {
const request = {
method: 'changePassword' as const,
params: [oldPassword, newPassword]
}
await this.rpc(request)
}
async signUpJoin (
email: string,
password: string,
first: string,
last: string,
inviteId: string
): Promise<WorkspaceLoginInfo> {
const request = {
method: 'signUpJoin' as const,
params: [email, password, first, last, inviteId]
}
return await this.rpc(request)
}
async join (email: string, password: string, inviteId: string): Promise<WorkspaceLoginInfo> {
const request = {
method: 'join' as const,
params: [email, password, inviteId]
}
return await this.rpc(request)
}
async createInviteLink (
exp: number,
emailMask: string,
limit: number,
role: AccountRole,
personId?: any
): Promise<string> {
const request = {
method: 'createInviteLink' as const,
params: [exp, emailMask, limit, role, personId]
}
return await this.rpc(request)
}
async checkJoin (inviteId: string): Promise<WorkspaceLoginInfo> {
const request = {
method: 'checkJoin' as const,
params: [inviteId]
}
return await this.rpc(request)
}
async getWorkspaceInfo (updateLastVisit: boolean = false): Promise<WorkspaceInfoWithStatus> {
const request = {
method: 'getWorkspaceInfo' as const,
params: updateLastVisit ? [true] : []
}
return this.flattenStatus(await this.rpc(request))
}
async getRegionInfo (): Promise<RegionInfo[]> {
const request = {
method: 'getRegionInfo' as const,
params: []
}
return await this.rpc(request)
}
async createWorkspace (name: string, region?: string): Promise<WorkspaceLoginInfo> {
const request = {
method: 'createWorkspace' as const,
params: [name, region]
}
return await this.rpc(request)
}
async signUpOtp (email: string, first: string, last: string): Promise<OtpInfo> {
const request = {
method: 'signUpOtp' as const,
params: [email, first, last]
}
return await this.rpc(request)
}
async signUp (email: string, password: string, first: string, last: string): Promise<LoginInfo> {
const request = {
method: 'signUp' as const,
params: [email, password, first, last]
}
return await this.rpc(request)
}
async login (email: string, password: string): Promise<LoginInfo> {
const request = {
method: 'login' as const,
params: [email, password]
}
return await this.rpc(request)
}
async getPerson (): Promise<Person> {
const request = {
method: 'getPerson' as const,
params: []
}
return await this.rpc(request)
}
async getSocialIds (): Promise<SocialId[]> {
const request = {
method: 'getSocialIds' as const,
params: []
}
return await this.rpc(request)
}
async workerHandshake (region: string, version: Data<Version>, operation: WorkspaceOperation): Promise<void> {
const request = {
method: 'workerHandshake' as const,
params: [region, version, operation]
}
await this.rpc(request)
}
async getPendingWorkspace (
region: string,
version: Data<Version>,
operation: WorkspaceOperation
): Promise<WorkspaceInfoWithStatus | null> {
const request = {
method: 'getPendingWorkspace' as const,
params: [region, version, operation]
}
const result = await this.rpc(request)
if (result == null) {
return null
}
return this.flattenStatus(result)
}
async updateWorkspaceInfo (
wsUuid: string,
event: string,
version: Data<Version>,
progress: number,
message?: string
): Promise<void> {
const request = {
method: 'updateWorkspaceInfo' as const,
params: [wsUuid, event, version, progress, message]
}
await this.rpc(request)
}
async getWorkspaceMembers (): Promise<WorkspaceMemberInfo[]> {
const request = {
method: 'getWorkspaceMembers' as const,
params: []
}
return await this.rpc(request)
}
async updateWorkspaceRole (account: string, role: AccountRole): Promise<void> {
const request = {
method: 'updateWorkspaceRole' as const,
params: [account, role]
}
await this.rpc(request)
}
async updateWorkspaceName (name: string): Promise<void> {
const request = {
method: 'updateWorkspaceName' as const,
params: [name]
}
await this.rpc(request)
}
async deleteWorkspace (): Promise<void> {
const request = {
method: 'deleteWorkspace' as const,
params: []
}
await this.rpc(request)
}
async findPerson (socialString: string): Promise<PersonUuid | undefined> {
const request = {
method: 'findPerson' as const,
params: [socialString]
}
return await this.rpc(request)
}
async listWorkspaces (region?: string | null, mode: WorkspaceMode | null = null): Promise<WorkspaceInfoWithStatus[]> {
const request = {
method: 'listWorkspaces' as const,
params: [region, mode]
}
return ((await this.rpc<any[]>(request)) ?? []).map((ws) => this.flattenStatus(ws))
}
async performWorkspaceOperation (
workspaceId: string | string[],
event: 'archive' | 'migrate-to' | 'unarchive' | 'delete',
...params: any
): Promise<boolean> {
const request = {
method: 'performWorkspaceOperation' as const,
params: [workspaceId, event, ...params]
}
return await this.rpc(request)
}
async updateBackupInfo (info: BackupStatus): Promise<void> {
const request = {
method: 'updateBackupInfo' as const,
params: [info]
}
await this.rpc(request)
}
async assignWorkspace (email: string, workspaceUuid: string, role: AccountRole): Promise<void> {
const request = {
method: 'assignWorkspace' as const,
params: [email, workspaceUuid, role]
}
await this.rpc(request)
}
async updateWorkspaceRoleBySocialId (socialKey: string, targetRole: AccountRole): Promise<void> {
const request = {
method: 'updateWorkspaceRoleBySocialId' as const,
params: [socialKey, targetRole]
}
await this.rpc(request)
}
}
async function retry<T> (retries: number, op: () => Promise<T>, delay: number = 100): Promise<T> {
let error: any
while (retries > 0) {
retries--
try {
return await op()
} catch (err: any) {
error = err
if (retries !== 0) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
}
throw error
}
+18
View File
@@ -0,0 +1,18 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
export * from './client'
export * from './types'
export * from './utils'
+30
View File
@@ -0,0 +1,30 @@
import { WorkspaceDataId, WorkspaceUuid, type AccountRole, type PersonUuid, type Timestamp } from '@hcengineering/core'
export interface LoginInfo {
account: PersonUuid
token?: string
}
/**
* @public
*/
export interface WorkspaceLoginInfo extends LoginInfo {
workspace: WorkspaceUuid // worspace uuid
workspaceDataId?: WorkspaceDataId
workspaceUrl: string
endpoint: string
token: string
role: AccountRole
}
export interface OtpInfo {
sent: boolean
retryOn: Timestamp
}
export interface RegionInfo {
region: string
name: string
}
export type WorkspaceOperation = 'create' | 'upgrade' | 'all' | 'all+backup'
@@ -13,26 +13,8 @@
// limitations under the License.
//
import { WorkspaceInfo } from '@hcengineering/account'
import type { LoginInfo, WorkspaceLoginInfo } from './types'
import config from './config'
export async function getWorkspaceInfo (token: string): Promise<WorkspaceInfo | undefined> {
const accountsUrl = config.AccountsUrl
const workspaceInfo = await (
await fetch(accountsUrl, {
method: 'POST',
keepalive: true,
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
method: 'getWorkspaceInfo',
params: []
})
})
).json()
return workspaceInfo.result as WorkspaceInfo
export function isWorkspaceLoginInfo (loginInfo: LoginInfo | WorkspaceLoginInfo): loginInfo is WorkspaceLoginInfo {
return (loginInfo as WorkspaceLoginInfo).workspace != null
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}
+1
View File
@@ -46,6 +46,7 @@
"@hcengineering/client": "^0.6.18",
"@hcengineering/client-resources": "^0.6.27",
"@hcengineering/collaborator-client": "^0.6.4",
"@hcengineering/account-client": "^0.6.0",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/text": "^0.6.5"
},
-64
View File
@@ -1,64 +0,0 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
/** @public */
export interface LoginInfo {
token: string
endpoint: string
confirmed: boolean
email: string
}
/** @public */
export interface WorkspaceLoginInfo extends LoginInfo {
workspace: string
workspaceId: string
}
export async function login (accountsUrl: string, user: string, password: string, workspace: string): Promise<string> {
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
method: 'login',
params: [user, password, workspace]
})
})
const result = await response.json()
return result.result?.token
}
export async function selectWorkspace (
accountsUrl: string,
token: string,
workspace: string
): Promise<WorkspaceLoginInfo> {
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token
},
body: JSON.stringify({
method: 'selectWorkspace',
params: [workspace, 'external']
})
})
const result = await response.json()
return result.result as WorkspaceLoginInfo
}
+31 -15
View File
@@ -12,9 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type Account,
type Class,
type Client,
type Data,
@@ -35,12 +33,15 @@ import {
Mixin,
MixinUpdate,
MixinData,
generateId
generateId,
PersonId,
buildSocialIdString,
WorkspaceUuid
} from '@hcengineering/core'
import client, { clientId } from '@hcengineering/client'
import { addLocation, getResource } from '@hcengineering/platform'
import { getClient as getAccountClient } from '@hcengineering/account-client'
import { login, selectWorkspace } from './account'
import { type ServerConfig, loadServerConfig } from './config'
import {
type MarkupFormat,
@@ -58,28 +59,42 @@ export async function connect (url: string, options: ConnectOptions): Promise<Pl
const config = await loadServerConfig(url)
const { endpoint, token } = await getWorkspaceToken(url, options, config)
return await createClient(url, endpoint, token, config, options)
const accountClient = getAccountClient(config.ACCOUNTS_URL, token)
const socialStrings = (await accountClient.getSocialIds()).map((si) => buildSocialIdString(si))
if (socialStrings.length === 0) {
throw new Error('No social ids found for the logged in user')
}
const wsLoginInfo = await accountClient.selectWorkspace(options.workspace)
if (wsLoginInfo === undefined) {
throw new Error(`Workspace ${options.workspace} not found`)
}
return await createClient(url, endpoint, token, wsLoginInfo.workspace, socialStrings[0], config, options)
}
async function createClient (
url: string,
endpoint: string,
token: string,
workspaceUuid: WorkspaceUuid,
user: PersonId,
config: ServerConfig,
options: ConnectOptions
): Promise<PlatformClient> {
addLocation(clientId, () => import(/* webpackChunkName: "client" */ '@hcengineering/client-resources'))
const { workspace, socketFactory, connectionTimeout } = options
const { socketFactory, connectionTimeout } = options
const clientFactory = await getResource(client.function.GetClient)
const connection = await clientFactory(token, endpoint, {
socketFactory,
connectionTimeout
})
const account = await connection.getAccount()
return new PlatformClientImpl(url, workspace, token, config, connection, account)
return new PlatformClientImpl(url, workspaceUuid, token, config, connection, user)
}
class PlatformClientImpl implements PlatformClient {
@@ -88,13 +103,13 @@ class PlatformClientImpl implements PlatformClient {
constructor (
private readonly url: string,
private readonly workspace: string,
private readonly workspace: WorkspaceUuid,
private readonly token: string,
private readonly config: ServerConfig,
private readonly connection: Client,
private readonly account: Account
private readonly user: PersonId
) {
this.client = new TxOperations(connection, account._id)
this.client = new TxOperations(connection, user)
this.markup = createMarkupOperations(url, workspace, token, config)
}
@@ -280,20 +295,21 @@ async function getWorkspaceToken (
): Promise<{ endpoint: string, token: string }> {
config ??= await loadServerConfig(url)
let token: string
let token: string | undefined
if ('token' in options) {
token = options.token
} else {
const { email, password, workspace } = options
token = await login(config.ACCOUNTS_URL, email, password, workspace)
const { email, password } = options
const loginInfo = await getAccountClient(config.ACCOUNTS_URL).login(email, password)
token = loginInfo.token
}
if (token === undefined) {
throw new Error('Login failed')
}
const ws = await selectWorkspace(config.ACCOUNTS_URL, token, options.workspace)
const ws = await getAccountClient(config.ACCOUNTS_URL, token).selectWorkspace(options.workspace)
if (ws === undefined) {
throw new Error('Workspace not found')
}
+12 -4
View File
@@ -13,7 +13,15 @@
// limitations under the License.
//
import { type Class, type Doc, type Markup, type Ref, concatLink, makeCollabId } from '@hcengineering/core'
import {
type Class,
type Doc,
type Markup,
type Ref,
WorkspaceUuid,
concatLink,
makeCollabId
} from '@hcengineering/core'
import { type CollaboratorClient, getClient } from '@hcengineering/collaborator-client'
import { parseMessageMarkdown, jsonToMarkup, markupToHTML, markupToMarkdown, htmlToMarkup } from '@hcengineering/text'
@@ -22,7 +30,7 @@ import { type MarkupOperations, type MarkupFormat, type MarkupRef } from './type
export function createMarkupOperations (
url: string,
workspace: string,
workspace: WorkspaceUuid,
token: string,
config: ServerConfig
): MarkupOperations {
@@ -36,13 +44,13 @@ class MarkupOperationsImpl implements MarkupOperations {
constructor (
private readonly url: string,
private readonly workspace: string,
private readonly workspace: WorkspaceUuid,
private readonly token: string,
private readonly config: ServerConfig
) {
this.refUrl = concatLink(this.url, `/browse?workspace=${workspace}`)
this.imageUrl = concatLink(this.url, `/files?workspace=${workspace}&file=`)
this.collaborator = getClient({ name: workspace }, token, config.COLLABORATOR_URL)
this.collaborator = getClient(workspace, token, config.COLLABORATOR_URL)
}
async fetchMarkup (
+4 -4
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { Blob, CollaborativeDoc, Markup, MarkupBlobRef, Ref, WorkspaceId, concatLink } from '@hcengineering/core'
import { Blob, CollaborativeDoc, Markup, MarkupBlobRef, Ref, WorkspaceUuid, concatLink } from '@hcengineering/core'
import { encodeDocumentId } from './utils'
/** @public */
@@ -54,20 +54,20 @@ export interface CollaboratorClient {
}
/** @public */
export function getClient (workspaceId: WorkspaceId, token: string, collaboratorUrl: string): CollaboratorClient {
export function getClient (workspaceId: WorkspaceUuid, token: string, collaboratorUrl: string): CollaboratorClient {
const url = collaboratorUrl.replaceAll('wss://', 'https://').replace('ws://', 'http://')
return new CollaboratorClientImpl(workspaceId, token, url)
}
class CollaboratorClientImpl implements CollaboratorClient {
constructor (
private readonly workspace: WorkspaceId,
private readonly workspace: WorkspaceUuid,
private readonly token: string,
private readonly collaboratorUrl: string
) {}
private async rpc<P, R>(document: CollaborativeDoc, method: string, payload: P): Promise<R> {
const workspace = this.workspace.name
const workspace = this.workspace
const documentId = encodeDocumentId(workspace, document)
const url = concatLink(this.collaboratorUrl, `/rpc/${encodeURIComponent(documentId)}`)
+2 -1
View File
@@ -65,6 +65,7 @@
"BlobContentType": "Typ obsahu",
"Relation": "Vztah",
"Relations": "Vztahy",
"AddRelation": "Přidat vztah"
"AddRelation": "Přidat vztah",
"PersonId": "Osoba"
}
}
+2 -1
View File
@@ -65,6 +65,7 @@
"BlobContentType": "Content type",
"Relation": "Relation",
"Relations": "Relations",
"AddRelation": "Add relation"
"AddRelation": "Add relation",
"PersonId": "Person"
}
}

Some files were not shown because too many files have changed in this diff Show More