Fix separation by productId (#2382)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-11-16 22:03:03 +07:00 committed by GitHub
parent 55e3cbea59
commit a8af08de88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 1198 additions and 930 deletions

7
.vscode/launch.json vendored
View File

@ -35,12 +35,13 @@
"type": "node",
"request": "launch",
"args": [
"src/index.ts"
"src/__start.ts"
],
"env": {
"MONGO_URL": "mongodb://localhost:27017",
"MONGO_URL": "mongodb://localhost:27018",
"SERVER_SECRET": "secret",
"TRANSACTOR_URL": "ws:/localhost:3333"
"TRANSACTOR_URL": "ws:/localhost:3333",
"ACCOUNT_PORT": "3000"
},
"runtimeArgs": [
"--nolazy",

View File

@ -64,6 +64,7 @@ specifiers:
'@rush-temp/login-assets': file:./projects/login-assets.tgz
'@rush-temp/login-resources': file:./projects/login-resources.tgz
'@rush-temp/middleware': file:./projects/middleware.tgz
'@rush-temp/minio': file:./projects/minio.tgz
'@rush-temp/model': file:./projects/model.tgz
'@rush-temp/model-activity': file:./projects/model-activity.tgz
'@rush-temp/model-all': file:./projects/model-all.tgz
@ -399,6 +400,7 @@ dependencies:
'@rush-temp/login-assets': file:projects/login-assets.tgz_typescript@4.8.4
'@rush-temp/login-resources': file:projects/login-resources.tgz_49b4785992daa3b61a639b2b31601e76
'@rush-temp/middleware': file:projects/middleware.tgz
'@rush-temp/minio': file:projects/minio.tgz
'@rush-temp/model': file:projects/model.tgz
'@rush-temp/model-activity': file:projects/model-activity.tgz_typescript@4.8.4
'@rush-temp/model-all': file:projects/model-all.tgz_typescript@4.8.4
@ -11135,7 +11137,7 @@ packages:
dev: false
file:projects/account.tgz:
resolution: {integrity: sha512-EZcey8t5zWFBsNNvGqJiaPLFgSs2JwaFVOd5qKw2sNo7KbMYdYtVWUNS/SVnDmF2TlzbIysdzZc39dYoyegHhA==, tarball: file:projects/account.tgz}
resolution: {integrity: sha512-8yjt4NrdmP1f+lfABJvi/UMWdJyh7o5ozzLsBxAOoNPPJdv2hfWGkQRlaix24WJY69UMXug587gbLi8kvl1JDQ==, tarball: file:projects/account.tgz}
name: '@rush-temp/account'
version: 0.0.0
dependencies:
@ -11184,7 +11186,7 @@ packages:
dev: false
file:projects/activity-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-hem1W1iTJU3wP58o4nHQ1BtG/K7sYtRGZPRG60N17LEHYEeKU3K0hnMHo17uCeT9w5EyEp5/3oZjvP5jyXU6Jw==, tarball: file:projects/activity-resources.tgz}
resolution: {integrity: sha512-XHOxvpcYfiLdYp0O1q6ji0g5cqK1WrcYb8e9G0HVFu7WzKhSJ7mvSZcfb5fngTL8XjTLaika8BVvh4Dfz+Al+A==, tarball: file:projects/activity-resources.tgz}
id: file:projects/activity-resources.tgz
name: '@rush-temp/activity-resources'
version: 0.0.0
@ -11285,7 +11287,7 @@ packages:
dev: false
file:projects/attachment-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-RC9+heJmAgXIBPY0P4mrE7BWaOS2pP1ddegWy5/TLHIi23XGAt02FRu0rYmNbxM9p9Z3hwW0dSGuHBpSzpailg==, tarball: file:projects/attachment-resources.tgz}
resolution: {integrity: sha512-9RxkG18ZVsrmz3scEahdgy0a8r8QCjWuh4zZ0ZxAun8QIy1SIw3pBR2HB2qzMergiZzK8YtZltxFEycjFggKeQ==, tarball: file:projects/attachment-resources.tgz}
id: file:projects/attachment-resources.tgz
name: '@rush-temp/attachment-resources'
version: 0.0.0
@ -11441,7 +11443,7 @@ packages:
dev: false
file:projects/board-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-wZB0AWu/DawUcePr1rCZRa61LwWg7pdT/i0gR017Z9O6HCrQP2H23wJtjn4CvyTE2HsAneKYDAB55PX2/4C1/g==, tarball: file:projects/board-resources.tgz}
resolution: {integrity: sha512-5YVKhvdOcaXKoVvMniN9nzBOzwUL0SR5klgOCm6iIdV1dKLui/07T1e86lFgVAdsPgtzxWJlvSEQ3G1M53B+lg==, tarball: file:projects/board-resources.tgz}
id: file:projects/board-resources.tgz
name: '@rush-temp/board-resources'
version: 0.0.0
@ -11476,7 +11478,7 @@ packages:
dev: false
file:projects/board.tgz:
resolution: {integrity: sha512-6kULPATq0+6/1mevbXuB430pPu072K21Pv1IczE0tXXaKuXwuaPV4nR4QbeD/BE+esD8XgOVE497rhYGnpD4nQ==, tarball: file:projects/board.tgz}
resolution: {integrity: sha512-md//1C1/iEupRv4ycc6d2a4MWORj/AMcIp3blZ2S+8ZEkIr/yUXS6oAEcW2S6IJQ8UPlhDhq35wBng5FrtVdeg==, tarball: file:projects/board.tgz}
name: '@rush-temp/board'
version: 0.0.0
dependencies:
@ -11518,7 +11520,7 @@ packages:
dev: false
file:projects/calendar-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-Wz8JrvayplUY+oNPxqiYa8asg6NtssCRTkRB85SLFvxuRKAwQASSXjuDphCvBOylMF/9dNsHaNWTQZ08OdRJ+g==, tarball: file:projects/calendar-resources.tgz}
resolution: {integrity: sha512-iW7MK0QNp0R5DZ1I/UlE+5bHccepUVGyOHzqzNh6z14qnIokE5BgrDtjoucYqPbTlDFv2UW1S5xiXNsydRAztA==, tarball: file:projects/calendar-resources.tgz}
id: file:projects/calendar-resources.tgz
name: '@rush-temp/calendar-resources'
version: 0.0.0
@ -11553,7 +11555,7 @@ packages:
dev: false
file:projects/calendar.tgz:
resolution: {integrity: sha512-X7whcXZsRvqicnxJPBHHINOI1YxmUGbERdxYdhNILkyDdQNyuduOpijPZiFUT094U8wyxd7pPDIGUh23wkXX1A==, tarball: file:projects/calendar.tgz}
resolution: {integrity: sha512-Z7WVKethTuqQJnE+RhrI7nNvNPrnLENofC/nA71QWyFqJPSw81QUMIPCcizhTksxSI5t2zDQiVjUaK1GEk5qRw==, tarball: file:projects/calendar.tgz}
name: '@rush-temp/calendar'
version: 0.0.0
dependencies:
@ -11595,7 +11597,7 @@ packages:
dev: false
file:projects/chunter-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-PCYdxv+j37WKeFIcQ/FrKcXs0j37H0+1ot0d6F2VkXIrTX0OvFGIZJG+xRAzk6wPqsEkjXt0loyR4P0FuY6Pqw==, tarball: file:projects/chunter-resources.tgz}
resolution: {integrity: sha512-tdUNzswIFdvN6b6bhqRPTn1KUjFNMvNvVYfRfr38NRRpoHGIOrL/fNVcDU8c+4y9p880JFvngFBopIFEp5GYVw==, tarball: file:projects/chunter-resources.tgz}
id: file:projects/chunter-resources.tgz
name: '@rush-temp/chunter-resources'
version: 0.0.0
@ -11631,7 +11633,7 @@ packages:
dev: false
file:projects/chunter.tgz:
resolution: {integrity: sha512-gYPNBzmHCCc/PmxpKzgRqRnUkcp0SLfEyP8Hr8sneriMFNjD0PfMjVzQLPPbRF51zpwoJ1Zr10+mUWJ8ScZP/A==, tarball: file:projects/chunter.tgz}
resolution: {integrity: sha512-dKNFBXk+iMygMyZfOYVOuoSLIEhNp1Jft1F1lXO3jonxEUzInGto6cU004XW2xX9blUM/l7MyF6LX7SnV3dZ3Q==, tarball: file:projects/chunter.tgz}
name: '@rush-temp/chunter'
version: 0.0.0
dependencies:
@ -11691,7 +11693,7 @@ packages:
dev: false
file:projects/contact-assets.tgz_typescript@4.8.4:
resolution: {integrity: sha512-1hp8KJRpc5+xgDMndnrHtzyGFI6tV3LDPAcVPDy7eMBOf/Rt67/QCzGG7txsA6pdNsf8rcnGZ8glfyYnU2pVwA==, tarball: file:projects/contact-assets.tgz}
resolution: {integrity: sha512-nTzb48LU1qn3ktHCeM/aIXxE5K+QhJyp21cOogImGLi9wp7CGHcmwaRHBasZ97qogD0uzNE0rhV2IbcNvC+57w==, tarball: file:projects/contact-assets.tgz}
id: file:projects/contact-assets.tgz
name: '@rush-temp/contact-assets'
version: 0.0.0
@ -11713,7 +11715,7 @@ packages:
dev: false
file:projects/contact-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-tiemPkkX5m+f8IpwQV8NoPJhYtfHNsUYemK3xgmT5/QbFKqekdky7GELmpq6fY9zkdXp1pKRU0jaoEC8s0w1Ow==, tarball: file:projects/contact-resources.tgz}
resolution: {integrity: sha512-MUTOkFTO+OF484QeG3bQXv4b6wd8GdjBoEcyikAybS5cnENj8ZYCXCXbXHcF7tRu8GHXrirez958V2TdIhwaUQ==, tarball: file:projects/contact-resources.tgz}
id: file:projects/contact-resources.tgz
name: '@rush-temp/contact-resources'
version: 0.0.0
@ -11903,7 +11905,7 @@ packages:
dev: false
file:projects/devmodel-resources.tgz_5536a2d3219f8677582bfc0330dae14a:
resolution: {integrity: sha512-oF7bkVohXrka8/IEBrjbK+JxQqsZ7OV101DjYb7cDePyj5ofnGEIv2r8DDmXO36H7zoQWpSSJjX3A+EUlBFo6w==, tarball: file:projects/devmodel-resources.tgz}
resolution: {integrity: sha512-EE1jWJBSvCdX6juiSh/zzAfrqkyBDWPV89fZVZH9Twfyae05shz+Guifvn0GXrHCc+z9zxcCUNOJa+vABJc8Aw==, tarball: file:projects/devmodel-resources.tgz}
id: file:projects/devmodel-resources.tgz
name: '@rush-temp/devmodel-resources'
version: 0.0.0
@ -11981,7 +11983,7 @@ packages:
dev: false
file:projects/document-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-K75XTawcHH1MSsC/OIFILTVWFdikC9rZACQD+TZgDGsk6cnzJRz7WaoYqj+2oPK/qwsRryi+/7amdKIg/1kytw==, tarball: file:projects/document-resources.tgz}
resolution: {integrity: sha512-Q4PGk2NShFmlCK3mStJC4ZgOCBEYs2cnWDM+oi6W9UioOE4joj/3KOP9Px/3294UWbq5eS+BzQHNtZ40h2ZukQ==, tarball: file:projects/document-resources.tgz}
id: file:projects/document-resources.tgz
name: '@rush-temp/document-resources'
version: 0.0.0
@ -12016,7 +12018,7 @@ packages:
dev: false
file:projects/document.tgz:
resolution: {integrity: sha512-jahDbKxsWybK48eLZUVfFGWtq0pJ8daCxTFABIhLnPZIDPQu8Ik7PM3mRxQY8u7sYurRJgl40E+a+ud+eg2TJA==, tarball: file:projects/document.tgz}
resolution: {integrity: sha512-ASeqqVDRZgKNc/fwCU3iyaiuaP8Wk+seON655YeQwhvbClARe9yy7+KKX7XzE2pIbPdCTxwUBIiqR+/u9rt5Vw==, tarball: file:projects/document.tgz}
name: '@rush-temp/document'
version: 0.0.0
dependencies:
@ -12103,7 +12105,7 @@ packages:
dev: false
file:projects/generator.tgz:
resolution: {integrity: sha512-4bFipFYTQCqnCbezi7dUjsyHwywpffCmekAwVkZ+S1+WHS9sNz8mI7vTpWdc1D4iikETukkO/aXkqoRXi3n2RQ==, tarball: file:projects/generator.tgz}
resolution: {integrity: sha512-knz6FRYXSNtm1MU/tTKpG1PwQvePi1uaNqNwKmWRURDg/59PESdFMQadueRbIdgJJ15tlJU4ki8yCHmO6l+n0Q==, tarball: file:projects/generator.tgz}
name: '@rush-temp/generator'
version: 0.0.0
dependencies:
@ -12161,7 +12163,7 @@ packages:
dev: false
file:projects/gmail-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-xaaL8WfttW0hFwVoD1CLyuaXGrRcQ/Dwkv9WIo9OGnrsdrjeSLcVvt+nR6u2TcoIVqWhYJEScrdEHL4DHmIz0A==, tarball: file:projects/gmail-resources.tgz}
resolution: {integrity: sha512-emQZ2qN1r74yxViTo8Uef5YWdTB9eK+bGvUdbMmtrNUSBL60+KdOgv0661cqmtZxIlPupCkA3PJpmiv+Nd8azQ==, tarball: file:projects/gmail-resources.tgz}
id: file:projects/gmail-resources.tgz
name: '@rush-temp/gmail-resources'
version: 0.0.0
@ -12196,7 +12198,7 @@ packages:
dev: false
file:projects/gmail.tgz:
resolution: {integrity: sha512-nr4baabaooIoYZIIgJe/SyM1mZwwBOCJu6IIlvnH8wAqEHMgu6akue7krMewBcHM+qFiMzuwVy1OMD6TwjfZfA==, tarball: file:projects/gmail.tgz}
resolution: {integrity: sha512-LOZ+NuO1daLQsEoaq1h9Y9ysB80lyHbVm8cFJaArhIb9J0L+Jm4Orx0fEmbKBNwPlS3fTwVkezAf5Gv7oeig8g==, tarball: file:projects/gmail.tgz}
name: '@rush-temp/gmail'
version: 0.0.0
dependencies:
@ -12238,7 +12240,7 @@ packages:
dev: false
file:projects/hr-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-9Y+8tgNwZRF5xyscGddizuYZ7g7ca5/Ga2gGuVYE5C7A5j8ODp4yhz9llCuLDL/kYEl4uuwHa2L0jpUIzNsXQg==, tarball: file:projects/hr-resources.tgz}
resolution: {integrity: sha512-WGtSBxKPXMjLGaqlH58ho5KqGGXAOat/pxd+xDDa/SRY23KYYc0N7t0a4td1uVbWE2lAw/DMIARrVYRlKbQTtg==, tarball: file:projects/hr-resources.tgz}
id: file:projects/hr-resources.tgz
name: '@rush-temp/hr-resources'
version: 0.0.0
@ -12273,7 +12275,7 @@ packages:
dev: false
file:projects/hr.tgz:
resolution: {integrity: sha512-JS/5Vkfw0MZaqWP8/cn8/Iqe7htNp0kWKZxVbg+oa4ztD6c9ECUY7GfFCFU2oKKrl+TLa8az2ycQpm8PgmlmqQ==, tarball: file:projects/hr.tgz}
resolution: {integrity: sha512-YVZ57Gu5PlmB1oi0bZCF2WUE+LV271qyMTDSJxO38tFUKtCcwylU28J1fcial2TlR2zdmzuu7JfzkQh8+k/Ktg==, tarball: file:projects/hr.tgz}
name: '@rush-temp/hr'
version: 0.0.0
dependencies:
@ -12371,7 +12373,7 @@ packages:
dev: false
file:projects/inventory-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-k7OWnh4GrGbAeVbHV2y/VGNX+c4d8N3okU1dUMOYc/rYPk+DmjWoHoC2POZ2QUSkUiv9gTqpRvLvc3mNumAcEQ==, tarball: file:projects/inventory-resources.tgz}
resolution: {integrity: sha512-icjBvRl/8K10Hv4JULUQzhomrNocRy5YRGDtOzUYSiN31mHygrgJMBbsY6Lo/JAYMYBIa2X8IKhqtn/Zlxt/Nw==, tarball: file:projects/inventory-resources.tgz}
id: file:projects/inventory-resources.tgz
name: '@rush-temp/inventory-resources'
version: 0.0.0
@ -12484,7 +12486,7 @@ packages:
dev: false
file:projects/lead-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-hnoDwg89HfQwSntwS6MqLO87j5533mEr7yqzB8UBTo5XkkUP0tHoop8jBTP5EkaPm4VC6itT4c5MISYGke2OOQ==, tarball: file:projects/lead-resources.tgz}
resolution: {integrity: sha512-QTtzdm5tOqm9IJlSjJAvjGezpTk81CQgDUnpz9KjRQhqpnkIZZQOwmsZ9afFTveU40u+yg7ul01lKn2vxF9naw==, tarball: file:projects/lead-resources.tgz}
id: file:projects/lead-resources.tgz
name: '@rush-temp/lead-resources'
version: 0.0.0
@ -12519,7 +12521,7 @@ packages:
dev: false
file:projects/lead.tgz:
resolution: {integrity: sha512-ZcvXbxP8xjUpIVllrH37R/JzhnlJPJ9g7cOqGa3zU03lRkPwvypWhrImz4RWpTXvW1hL5hyp7VZBYx8s5KlXOg==, tarball: file:projects/lead.tgz}
resolution: {integrity: sha512-xR3SR5JiLaiXNTXk4h2t4h4/A8hqwbX0EG41lxHdC3AoBsGHuQ2XCW4KozmhgRZhqmycCCpFSpWrUzIu0OG8dw==, tarball: file:projects/lead.tgz}
name: '@rush-temp/lead'
version: 0.0.0
dependencies:
@ -12561,7 +12563,7 @@ packages:
dev: false
file:projects/login-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-bx9MRAon/n8GKBvSzSHBRBeLHLrMiREcKHA5PzkPb84aCSFRRN4GxbuX3QLWWM2xyem2OW0iOb2/VdV2kamJnA==, tarball: file:projects/login-resources.tgz}
resolution: {integrity: sha512-BQLnglPIn6mMQ3GpBjQylOOb8VRY9znhgpeuCqwP5YlMoGLV/conYKpZwjrSzF55GnTlLaYxJdaVtnQrljThdA==, tarball: file:projects/login-resources.tgz}
id: file:projects/login-resources.tgz
name: '@rush-temp/login-resources'
version: 0.0.0
@ -12635,8 +12637,30 @@ packages:
- supports-color
dev: false
file:projects/minio.tgz:
resolution: {integrity: sha512-VoxVx9EB3UAH22AGrPedW7GCOMvELT35uTIjYoJM91wAg2AeN2yJBTpgoStYCZVIWT9kmCbgGE6+L2Vi0PT7fA==, tarball: file:projects/minio.tgz}
name: '@rush-temp/minio'
version: 0.0.0
dependencies:
'@rushstack/heft': 0.47.11
'@types/heft-jest': 1.0.3
'@types/minio': 7.0.14
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
eslint: 8.27.0
eslint-config-standard-with-typescript: 23.0.0_c9fe9619f50f4e82337a86c3af25e566
eslint-plugin-import: 2.26.0_eslint@8.27.0
eslint-plugin-n: 15.5.1_eslint@8.27.0
eslint-plugin-promise: 6.1.1_eslint@8.27.0
minio: 7.0.32
prettier: 2.7.1
typescript: 4.8.4
transitivePeerDependencies:
- supports-color
dev: false
file:projects/model-activity.tgz_typescript@4.8.4:
resolution: {integrity: sha512-FnAjRYTmHMV4mkSb0kq6Oj3F/dc355Hapwx7ztOTwpnTgSkqnguQIiMxLvRmuZVM7/VgFhPj3n0A9NWtPFa4Gg==, tarball: file:projects/model-activity.tgz}
resolution: {integrity: sha512-ltIKPc9CmnmlRJ6HQY9zKwYNYMem68EtF1OKKoUVtZ4qckpvVbz4AmGmXgq/CHF6iOIy+2pqI7z1BIQvRWXXEA==, tarball: file:projects/model-activity.tgz}
id: file:projects/model-activity.tgz
name: '@rush-temp/model-activity'
version: 0.0.0
@ -12724,7 +12748,7 @@ packages:
dev: false
file:projects/model-board.tgz_typescript@4.8.4:
resolution: {integrity: sha512-Y2ojGzljGwQ9Txndgmepy4xICqv+hJxn0hkFWcmRZvqQWLoCCas8c33sElYC/oT7fRC3zQwNWNfkV6VppoT4Bw==, tarball: file:projects/model-board.tgz}
resolution: {integrity: sha512-PBcE6FlUziG9LLtIh2DNEERb67xKf0CyV0TjYAXc/bK5+MGpj5+6Nqut95PG9a7lmOUF5slPej/2u7UxKSPcEw==, tarball: file:projects/model-board.tgz}
id: file:projects/model-board.tgz
name: '@rush-temp/model-board'
version: 0.0.0
@ -12745,7 +12769,7 @@ packages:
dev: false
file:projects/model-calendar.tgz_typescript@4.8.4:
resolution: {integrity: sha512-BmnLqC+uaySe8LfLrPn5I6/akmNe3Clg0OTUhRwNjlP8OZBUmdqD76PdMvb+h7xi2Hnz+PXE8d8XgmEayV+cgw==, tarball: file:projects/model-calendar.tgz}
resolution: {integrity: sha512-yVeJUcUz8gS5CkEENilac9nkgOwlCJK7FY/A9f2Pm5EwTKfdvvnoAmM2xoQp3DOEQRI97qog92M3jxBBzOi1qw==, tarball: file:projects/model-calendar.tgz}
id: file:projects/model-calendar.tgz
name: '@rush-temp/model-calendar'
version: 0.0.0
@ -12766,7 +12790,7 @@ packages:
dev: false
file:projects/model-chunter.tgz_typescript@4.8.4:
resolution: {integrity: sha512-B7obo6hYDAkso5X5DX/2LikbY/9n55pZobzHFk8+NH6xaIFjvj6KfHkww5cpmitidaB04sw1TQypIgt6clTiqQ==, tarball: file:projects/model-chunter.tgz}
resolution: {integrity: sha512-WBKyQoFvo5wdEG4YKXq+OZGggI7dptfEPUte8LXYhgng+Bx/oYE3wDTMqTOW5Lo8/Tp2ZzQ4Wf0qEq/wd7cfcQ==, tarball: file:projects/model-chunter.tgz}
id: file:projects/model-chunter.tgz
name: '@rush-temp/model-chunter'
version: 0.0.0
@ -12787,7 +12811,7 @@ packages:
dev: false
file:projects/model-contact.tgz_typescript@4.8.4:
resolution: {integrity: sha512-1LXDgoRC+p48CfA8Dhk/JuUttYtwkJXaXADwFcvDJGLmWN062SEHy5mOCB56RcYxbBZvMOuRbKKkdZ7u678jzw==, tarball: file:projects/model-contact.tgz}
resolution: {integrity: sha512-skK2ivL1u7OjOqFKrwaDAUsFKYisT34pJohI8uUrRB7d/O4GtLev+C5zRhAy/d6f/yvO46r96gEOnOLCQVqY8A==, tarball: file:projects/model-contact.tgz}
id: file:projects/model-contact.tgz
name: '@rush-temp/model-contact'
version: 0.0.0
@ -12831,7 +12855,7 @@ packages:
dev: false
file:projects/model-demo.tgz_typescript@4.8.4:
resolution: {integrity: sha512-bGYWsKGMQt8TBsZUstuFnm2pFw60vkiUD7qZyucUyFgqpvPxkTPRXYSluL8nY+7Frv923EEbnogOGgn++uaQ1Q==, tarball: file:projects/model-demo.tgz}
resolution: {integrity: sha512-wcDEJcDjcxjJ5wxa/OlG/OIBxNagBH5UmiC2MIj1CE6Cr+IufMYM3e+dOuAXBZe1IB8SZSmV5MF006dRwKo9Gg==, tarball: file:projects/model-demo.tgz}
id: file:projects/model-demo.tgz
name: '@rush-temp/model-demo'
version: 0.0.0
@ -12852,7 +12876,7 @@ packages:
dev: false
file:projects/model-document.tgz_typescript@4.8.4:
resolution: {integrity: sha512-erlRfCr8MOH6uLf3EdKyPSHQc0SOkQ3UKf38lNfz4lxWV77z5NDMzeV5ReoApYL29nQuFd17nmgYa8uqI6wlEg==, tarball: file:projects/model-document.tgz}
resolution: {integrity: sha512-8gybSaEK3nD4LoxikWLsHdY9kcUF2DiPmn5eWG7wRhKKBcuoyuDfuItqB9WpEoNViiAVgNTIftAiLZnIOcTFpw==, tarball: file:projects/model-document.tgz}
id: file:projects/model-document.tgz
name: '@rush-temp/model-document'
version: 0.0.0
@ -12873,7 +12897,7 @@ packages:
dev: false
file:projects/model-gmail.tgz_typescript@4.8.4:
resolution: {integrity: sha512-iIiCPdHDCd183h52Gp/f4E32zWzgWhmhl1DYE6nYi1P/jZTFDlJcImdwJnDJ/S69EdBAoPsn4QAWoX+1GLZTsw==, tarball: file:projects/model-gmail.tgz}
resolution: {integrity: sha512-1N6oNODRhzjbZ92q1bTNu44VD4AzQLodNva0IzcswlhqPnGSPfpK+eeaz2dJqcdj6Ob5RpjEc2LKNdt5xysM9Q==, tarball: file:projects/model-gmail.tgz}
id: file:projects/model-gmail.tgz
name: '@rush-temp/model-gmail'
version: 0.0.0
@ -12894,7 +12918,7 @@ packages:
dev: false
file:projects/model-hr.tgz_typescript@4.8.4:
resolution: {integrity: sha512-/pKwQmDLkegCRfAcbSVrRHode4trh58qMkNU6NLyy2v+RGOC4lj+/l93+2nD6vS6XT4dPdOvEgjTAIgGPec+/Q==, tarball: file:projects/model-hr.tgz}
resolution: {integrity: sha512-EmKSttuNIARD0H5JMl/GL2j38po8Be7sf4VDhY5yTp4/aaUAk5Yr3ANc+MDmm2ZLnemQWZXM3VJS8rfAoiX4sQ==, tarball: file:projects/model-hr.tgz}
id: file:projects/model-hr.tgz
name: '@rush-temp/model-hr'
version: 0.0.0
@ -12915,7 +12939,7 @@ packages:
dev: false
file:projects/model-inventory.tgz_typescript@4.8.4:
resolution: {integrity: sha512-aSv3XnWQ/x6y+KgH5/uPhB+OTsMBt40CvDoBGuVUV7Iza8a/5LLoVch3uPq03UyNimvmu17s9ByV3JSMLrLfGw==, tarball: file:projects/model-inventory.tgz}
resolution: {integrity: sha512-X81p18418UhgRKOgvd3Eo7mAwnfmwqhItWq/gz8H4aBWd35Ov7E2Xkl1cReo0OrahqXCXXPLGWyWYdEzce9FTA==, tarball: file:projects/model-inventory.tgz}
id: file:projects/model-inventory.tgz
name: '@rush-temp/model-inventory'
version: 0.0.0
@ -12936,7 +12960,7 @@ packages:
dev: false
file:projects/model-lead.tgz_typescript@4.8.4:
resolution: {integrity: sha512-5BtncJVFf4Mdb1bPRUXA3a9C5dJSwF7Zn+tjsW3OGDTroRGt9nnqMwqX2+e/CKYMTOiYDalIHlp6MuobZut3hw==, tarball: file:projects/model-lead.tgz}
resolution: {integrity: sha512-6zs+yoagGEV9C5h/i2YKtsrH78m+ACIWsuQ97/rnS4p7ieuNUo0NMZkD8SPKBBOwt2H3jK1QcLoPXQ1fLKjmQA==, tarball: file:projects/model-lead.tgz}
id: file:projects/model-lead.tgz
name: '@rush-temp/model-lead'
version: 0.0.0
@ -13020,7 +13044,7 @@ packages:
dev: false
file:projects/model-recruit.tgz_typescript@4.8.4:
resolution: {integrity: sha512-8D3C5KRAGvCXhF7PbWRuAuGDxgYxM50wTrs4oFrBBitNgDoMEVJtlUs0NmxTZcBejT6TuvL1GIZ5AdzxosaNsg==, tarball: file:projects/model-recruit.tgz}
resolution: {integrity: sha512-VE7ozm46NXmnFFFJ/8bT32JqDkgB6iIfK87GHU/2faBJECPmbLXGbsiiT3o+IbQWPqNQzkNTTPUQzCz9RGnHpA==, tarball: file:projects/model-recruit.tgz}
id: file:projects/model-recruit.tgz
name: '@rush-temp/model-recruit'
version: 0.0.0
@ -13124,7 +13148,7 @@ packages:
dev: false
file:projects/model-server-contact.tgz_typescript@4.8.4:
resolution: {integrity: sha512-V/QenOWUhRg5f8yNSccvIsZP3y6VFPNtCmMDvO/KCjIHWdddMmlOgGbV0SzWp+0rfYXpt5XfDsUqaaFTqXelCw==, tarball: file:projects/model-server-contact.tgz}
resolution: {integrity: sha512-/0i0O2KtsAbmaqfqgC7nroFMRAZxkfhalGrCrrj2E+aOnzObvmtluv/SArW1MlfpVERblUuT/AYmekUqcFYBNQ==, tarball: file:projects/model-server-contact.tgz}
id: file:projects/model-server-contact.tgz
name: '@rush-temp/model-server-contact'
version: 0.0.0
@ -13166,7 +13190,7 @@ packages:
dev: false
file:projects/model-server-gmail.tgz_typescript@4.8.4:
resolution: {integrity: sha512-SX6g9Go7KIAcTinSGdpC4QbxqemB6WpWLrGC/dIWtg8R1tGmZdZLuNdhjybsRy8fO/COaeSInKxEYO1fhptSRw==, tarball: file:projects/model-server-gmail.tgz}
resolution: {integrity: sha512-yur2GVjsax/AYLft85HImrjWqywPJO163qbRwaTYk3wn5Spr1kW0bVcnsZcXg0f9iRHkMDJOIpRY2P1FO/GVuw==, tarball: file:projects/model-server-gmail.tgz}
id: file:projects/model-server-gmail.tgz
name: '@rush-temp/model-server-gmail'
version: 0.0.0
@ -13355,7 +13379,7 @@ packages:
dev: false
file:projects/model-server-telegram.tgz_typescript@4.8.4:
resolution: {integrity: sha512-9ll09PUnHtFPEFmo4SO57sGc8+6fBVAV4gcFoL+5swtX3v159CzXaUGLIiUgG6GclEpd9BIMrHq2tHH1f7AAIA==, tarball: file:projects/model-server-telegram.tgz}
resolution: {integrity: sha512-4z3rQykurAKm3Zo/lP1q8FB1geAj4iOj51DGbo1occQ6AQhS03qqrHoqsGm858btLu2FI0Wbps8H37yJo8UlcQ==, tarball: file:projects/model-server-telegram.tgz}
id: file:projects/model-server-telegram.tgz
name: '@rush-temp/model-server-telegram'
version: 0.0.0
@ -13439,7 +13463,7 @@ packages:
dev: false
file:projects/model-task.tgz_typescript@4.8.4:
resolution: {integrity: sha512-U9kGIuNhrWKxoQAioRp2WqYj7v+4aIfoGI6uKVFGQFfuUoVOq7dL/NZDAnw6M3g/GpJxQOB4yalevRrgkaGNVA==, tarball: file:projects/model-task.tgz}
resolution: {integrity: sha512-ijA6O177qrq9Mz5oJf02ESaHZgChn7zSWsbj9vL6EssvGMjhNASfo+ilsJ10ZwLxOC8QFh0h+IiSbWkiyNRnbw==, tarball: file:projects/model-task.tgz}
id: file:projects/model-task.tgz
name: '@rush-temp/model-task'
version: 0.0.0
@ -13460,7 +13484,7 @@ packages:
dev: false
file:projects/model-telegram.tgz_typescript@4.8.4:
resolution: {integrity: sha512-MCDcZMHGd1Mu7F8Rxb7qMfWM/9NnLxJo5aAHByEZGvFvZVc42nj+P5RrQps52bcUcwGA92wELWi6T+EljoL44g==, tarball: file:projects/model-telegram.tgz}
resolution: {integrity: sha512-JBPt3wBuXvvwjXC4XzHpO/0ILZWc2pcvQfjCjtSMtKNhXJslC7OQlThc2pRTYN8ZCP/CSbnzw2xoX6u3khSkNg==, tarball: file:projects/model-telegram.tgz}
id: file:projects/model-telegram.tgz
name: '@rush-temp/model-telegram'
version: 0.0.0
@ -13523,7 +13547,7 @@ packages:
dev: false
file:projects/model-tracker.tgz_typescript@4.8.4:
resolution: {integrity: sha512-XuNhqGceTe6+ymCYOc/tOnBsbPEJy1ufeAVT9T1NFWLwIbxBsP1txvbiyu1R4r1AVZ4QjJu/Be1kQBKQGyJGbQ==, tarball: file:projects/model-tracker.tgz}
resolution: {integrity: sha512-ZLbvHWpvbUBJLMlUDmhAJkoYE5pJx1V73BmM2EmPhS7pBpFh1enPXqQTW4M+0UotnbKBCpkssq3+1NYMoK+frQ==, tarball: file:projects/model-tracker.tgz}
id: file:projects/model-tracker.tgz
name: '@rush-temp/model-tracker'
version: 0.0.0
@ -13565,7 +13589,7 @@ packages:
dev: false
file:projects/model-workbench.tgz_typescript@4.8.4:
resolution: {integrity: sha512-IJiF3ij2t4uD8Y5s7v+GC05XTCP0QwifaThVQWHWLroHXkXXXPSXmt08YpLYkBs1LPV9bCSkfAKXpZbqBPPa/Q==, tarball: file:projects/model-workbench.tgz}
resolution: {integrity: sha512-Dssj/dje85sBveIduaksgpY4UHNg08a8foinnYSmW1JaTqdbvOrdfKvadE/knc3C6YaCzcM4zwrl2YIBoUjy6w==, tarball: file:projects/model-workbench.tgz}
id: file:projects/model-workbench.tgz
name: '@rush-temp/model-workbench'
version: 0.0.0
@ -13653,7 +13677,7 @@ packages:
dev: false
file:projects/notification-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-9lZ01h6+oSEL1t+QgLXy97qgJERc1oOR9EXNfhv1xWoCu6sjXe10GPmlMe4sGq/YkOS9LqnvuGLKTgXXcGzOMA==, tarball: file:projects/notification-resources.tgz}
resolution: {integrity: sha512-/5WLnwCSppaUhTK5CBTbIdbYEsHQxgvaLoQd/DrI7m0Axuve9OMcmOO7rseG3FyazklV3xZJboWYFxNBdYNPVA==, tarball: file:projects/notification-resources.tgz}
id: file:projects/notification-resources.tgz
name: '@rush-temp/notification-resources'
version: 0.0.0
@ -13807,7 +13831,7 @@ packages:
dev: false
file:projects/pod-account.tgz:
resolution: {integrity: sha512-XdRcITa4VLyzbj6jon8nbgIgAP+l+06Pvc65Sf54wADzBWFO27AX/BhZtIlhL5eCAMMrE2Tidh52xPDoXkbXpg==, tarball: file:projects/pod-account.tgz}
resolution: {integrity: sha512-n507oMGA9w0oyJHDG4Ct65c2q/reFMbeOYkqd8GLrV15ggrNObeMt21vKuGmNzcoa275qIUeUdOY5xbDuR4FQg==, tarball: file:projects/pod-account.tgz}
name: '@rush-temp/pod-account'
version: 0.0.0
dependencies:
@ -14024,7 +14048,7 @@ packages:
dev: false
file:projects/presentation.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-BRrMCOYZQWq7l4bHtorlsbF3Vd1xC3d1+HFwxAapz+2dWVdzTZa9LWbUnxdgOdVKnoPNEBshBb7yweuAz+QYvQ==, tarball: file:projects/presentation.tgz}
resolution: {integrity: sha512-VellyX1RtijHjE5/zTtkkxnnf6XEuOs+Miwa2GRBuPtNGzLho+fx78CgA3TNj12zv+v40N5KRgRJQZyGE+LyEg==, tarball: file:projects/presentation.tgz}
id: file:projects/presentation.tgz
name: '@rush-temp/presentation'
version: 0.0.0
@ -14060,7 +14084,7 @@ packages:
dev: false
file:projects/prod.tgz_b3a81ceaadec606c0eb174aef12a6049:
resolution: {integrity: sha512-L/+vs3539DkLEsndUMgU7L7LtdncWMbyKJTqNA4TWOIM5kOK4cLMqKqufG+sM/9Rjr5TMUpy2LmgwgNwS5tnPA==, tarball: file:projects/prod.tgz}
resolution: {integrity: sha512-j3kGErL/B4HOsYiCJ3qCdbVeyhLMmu4Ajz0a4a6Zu4LoNaiwKlLvYbD5qiJiXu5LARkmGFCFABw7yioFLLBnWw==, tarball: file:projects/prod.tgz}
id: file:projects/prod.tgz
name: '@rush-temp/prod'
version: 0.0.0
@ -14150,7 +14174,7 @@ packages:
dev: false
file:projects/recruit-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-BOswQ3tPI0sMMChbQBrca0qGo1nfFTS/UOMyqZYLU1vEQTPXIBwu7Eq9OmvyLRUYrHPriIrodEQ+um7246gYgQ==, tarball: file:projects/recruit-resources.tgz}
resolution: {integrity: sha512-xVQquoInxUPu3415Qcuwl5DctHN6soWJtUyHmMy+jZffMvxovcsHsk44bxebSzRNv8wtrShmjKcGGhJ643MZYg==, tarball: file:projects/recruit-resources.tgz}
id: file:projects/recruit-resources.tgz
name: '@rush-temp/recruit-resources'
version: 0.0.0
@ -14187,7 +14211,7 @@ packages:
dev: false
file:projects/recruit.tgz:
resolution: {integrity: sha512-V2+cf6AZI3Aw40vMEYBi0aQCR5gQTvuoR1dyi3q61B/zs9GZx6EV/plInET8Z7q+OtXm7PfSwR4dH8+UxGF1Ow==, tarball: file:projects/recruit.tgz}
resolution: {integrity: sha512-0kgdxh5Bjz8Z9oZjf1+9fYdgepnMouOtRqTlstB9iuABgoeLVK1fEUevxwCnu9DC7ViEsm600JQ0DtWoKiprrg==, tarball: file:projects/recruit.tgz}
name: '@rush-temp/recruit'
version: 0.0.0
dependencies:
@ -14271,7 +14295,7 @@ packages:
dev: false
file:projects/server-backup.tgz:
resolution: {integrity: sha512-8SeFK5Vj1yuAZ25nnV4dlt15yHFzF743DF9vF/TLxc4D7PaTEnCo6K2RcHRFX9s9ua8bh0fWwOFnYxBoAQYFkg==, tarball: file:projects/server-backup.tgz}
resolution: {integrity: sha512-snkRz4+41T/IJEpLmAnc9lUqo+K1xa0jkpcB5wdg1YSG+XQupGOuJ4Lu6V63+KrHEbWpircaTNL9gFEHQsWl5w==, tarball: file:projects/server-backup.tgz}
name: '@rush-temp/server-backup'
version: 0.0.0
dependencies:
@ -14337,7 +14361,7 @@ packages:
dev: false
file:projects/server-chunter-resources.tgz:
resolution: {integrity: sha512-eK6az//EWwcQnoe9BJUmTW5lVwx1m3zlqEYfSw400E1U+mCs1tmoxQrYRduY1x9rcOObwEjLqGfVRU9RHe/aKw==, tarball: file:projects/server-chunter-resources.tgz}
resolution: {integrity: sha512-G3CbPm0EUXqt9O3K6j4/LZ/7NoXP1cPn60A7Y+YG4U/sWeAIfycKosrL6FLDlucMz3+vZSeX6wK8W5VezBnk6g==, tarball: file:projects/server-chunter-resources.tgz}
name: '@rush-temp/server-chunter-resources'
version: 0.0.0
dependencies:
@ -14378,7 +14402,7 @@ packages:
dev: false
file:projects/server-contact-resources.tgz:
resolution: {integrity: sha512-9l0juRGQ7OMxnhOBHsvby5gzX3xC3fkKJJ9IQ9HKr+PmoZRF+fkeorBF5ttPnMva6rtY/uV0ukSH0pnOWxgdOA==, tarball: file:projects/server-contact-resources.tgz}
resolution: {integrity: sha512-RmqLUSQSZYSc1UFyU0RmZ7b7IBMT0rrbF3B1KpGcnFbu+sduTPW5byef5DnRLIDX2YlNvejYsfXssBCNk0LU/w==, tarball: file:projects/server-contact-resources.tgz}
name: '@rush-temp/server-contact-resources'
version: 0.0.0
dependencies:
@ -14442,7 +14466,7 @@ packages:
dev: false
file:projects/server-gmail-resources.tgz:
resolution: {integrity: sha512-rRnrXbwpcT6AqzWz+yDmfy0XuVgUWmz84Br2bZExPLpJwIdyvt7Wbn1kOqJfDRhsws6LRQb18vXn6xe/aKrx5w==, tarball: file:projects/server-gmail-resources.tgz}
resolution: {integrity: sha512-Kg6JAqy3b1OhPjbe6VCHgb6XZOm2Odj4lyFnxE/L8mZ+9UPRXYCzsrtGvLmjcHPYf4MmwSl5IAMok5D8vhBh0Q==, tarball: file:projects/server-gmail-resources.tgz}
name: '@rush-temp/server-gmail-resources'
version: 0.0.0
dependencies:
@ -14483,7 +14507,7 @@ packages:
dev: false
file:projects/server-hr-resources.tgz:
resolution: {integrity: sha512-MdFafFBbo3+AeGGi2hgKYOFDawLzjNUTcShGD3/SPKBqN7bJMrBQGaLHCw9O+7DatauGM+pIq/FwJi1a850CAA==, tarball: file:projects/server-hr-resources.tgz}
resolution: {integrity: sha512-ISPKgIxsz2z+AWmDLhfh4LSeORVewQv7pWa9QSJd2ZcMgg6ovZ3GOiX48PvzMGbuQ2jMqnQ4et3AUvDqn8jXDA==, tarball: file:projects/server-hr-resources.tgz}
name: '@rush-temp/server-hr-resources'
version: 0.0.0
dependencies:
@ -14524,7 +14548,7 @@ packages:
dev: false
file:projects/server-inventory-resources.tgz:
resolution: {integrity: sha512-uGp1LUr1aE7sIc4TIFa1+gx4gqps4PGF+LeGBuKVd06Nr0lqXRHwARKwcE8eiINVNc8kKo6ZC4PzV2ZnrNw21Q==, tarball: file:projects/server-inventory-resources.tgz}
resolution: {integrity: sha512-PHYWPX4xwfs2lob/d0j+uvPW/fPnjwP3Gs4SbAPIQudT+RYO5/8+sjvVh1pHFuJm6iOHoDAgRfukaD17rBIr5w==, tarball: file:projects/server-inventory-resources.tgz}
name: '@rush-temp/server-inventory-resources'
version: 0.0.0
dependencies:
@ -14565,7 +14589,7 @@ packages:
dev: false
file:projects/server-lead-resources.tgz:
resolution: {integrity: sha512-GH2zRy02F0/XEaK26uBrZFhd1YDfY791rszTd9rrjtkLwMDnXHF+drPPv3YZOZ7cah3NwSNnqnTAwOpJltnPzA==, tarball: file:projects/server-lead-resources.tgz}
resolution: {integrity: sha512-gsUYeV01/K2yXOU010jq8lW3GT49n0CjBU3RTc0xDb3WAukk9kLt9ukz397XAEfWGK40rltKYP5l+44CkJxcAA==, tarball: file:projects/server-lead-resources.tgz}
name: '@rush-temp/server-lead-resources'
version: 0.0.0
dependencies:
@ -14606,7 +14630,7 @@ packages:
dev: false
file:projects/server-notification-resources.tgz:
resolution: {integrity: sha512-FC05id1ogET5SgZypREim3huQwOt44WokPAg9vr5FlL7/AhFArJ9ChQJpNy1woSxf1Hf0v0ZywMx6FQDIqEe3w==, tarball: file:projects/server-notification-resources.tgz}
resolution: {integrity: sha512-UnY/SBcWBaxORRl0oN+jMG6FHsEfs9oiErCZSqvqSeZezKWjxE1pqB80zZC9vMa2pGSPzljqWyzKLbzP+7qQhw==, tarball: file:projects/server-notification-resources.tgz}
name: '@rush-temp/server-notification-resources'
version: 0.0.0
dependencies:
@ -14626,7 +14650,7 @@ packages:
dev: false
file:projects/server-notification.tgz:
resolution: {integrity: sha512-O4cSZKuzK9PnQpIVzGnOQd2nGwWnAORjNyqRbAr8OvCfcK1P7rp9Q6tMr8MyjftEot/age3PPmfBPvqQ28ac7g==, tarball: file:projects/server-notification.tgz}
resolution: {integrity: sha512-YZlf0UP5kL0NAzT42JJmlSFApgDCiNiW/YHXiNey5ZMoWSRL3XP7hepEBQVcIoJkR0Gks+3ppwQPdxty43MTVw==, tarball: file:projects/server-notification.tgz}
name: '@rush-temp/server-notification'
version: 0.0.0
dependencies:
@ -14668,7 +14692,7 @@ packages:
dev: false
file:projects/server-recruit-resources.tgz:
resolution: {integrity: sha512-HGH2H8uKiAD2hkOXf/SybyVDwMXd6faqaj89ZbJ9TJ2nGGXObFvW7gVIIJB+3trzTa8cOUgQHHQzU0R12ZQAfQ==, tarball: file:projects/server-recruit-resources.tgz}
resolution: {integrity: sha512-iPb7WAwVpg3XlbnztUzulyySQ0Uh5M20faBc4nkyHYXkee2jKtAjFT+X20Ht+2lVzk1u3PJ2AenOSJXgftipwA==, tarball: file:projects/server-recruit-resources.tgz}
name: '@rush-temp/server-recruit-resources'
version: 0.0.0
dependencies:
@ -14709,7 +14733,7 @@ packages:
dev: false
file:projects/server-setting-resources.tgz:
resolution: {integrity: sha512-PjjNR2MskZHvWLJl9vONhQ+QgM9+uM/WOBZOMymDsyPd8rCHhBDaOFXe9cHbDGpAz81qoT4d4EeRwfYpZBczKg==, tarball: file:projects/server-setting-resources.tgz}
resolution: {integrity: sha512-ybG+7dgX8uT16IPRZsvS8WctWNcrEziDlw59lf3ah3c4B5UaVPWyCy/pNKIC/HZNsa+GXQ8izUteURBVHuloig==, tarball: file:projects/server-setting-resources.tgz}
name: '@rush-temp/server-setting-resources'
version: 0.0.0
dependencies:
@ -14791,7 +14815,7 @@ packages:
dev: false
file:projects/server-task-resources.tgz:
resolution: {integrity: sha512-KtkbvfRH++JZX32+o9ZHqnRfYx+w2Y93URNbmwlC2hTmvpN1/+2IFjrVb/P4465D0LkLQpUZH3Y8FyG72Vz68Q==, tarball: file:projects/server-task-resources.tgz}
resolution: {integrity: sha512-VK0GdhbZ/hjVcV35GGjAsSfFvRblhWy/oOinB58S/m8dPMqcYKIJlUIvH4KDBM+wT79iKs91Y0T9unePeeLCPA==, tarball: file:projects/server-task-resources.tgz}
name: '@rush-temp/server-task-resources'
version: 0.0.0
dependencies:
@ -14832,7 +14856,7 @@ packages:
dev: false
file:projects/server-telegram-resources.tgz:
resolution: {integrity: sha512-b13BJhX2STuToJhP6lg0RwE9uVmLNp1t9QxDAgck7h0Fy8768RESGcpAHpew8bDZhlI/MqBKo6+4rmFfbSALQQ==, tarball: file:projects/server-telegram-resources.tgz}
resolution: {integrity: sha512-w+aKvtg2SEfBGHUgBfKbUpsxBZufLNqBYFJUVCCLr9VyT+OFcLLrGnXsOAAnaOwQ4Qgf5v1jAivSSuF3sM/jiQ==, tarball: file:projects/server-telegram-resources.tgz}
name: '@rush-temp/server-telegram-resources'
version: 0.0.0
dependencies:
@ -14897,7 +14921,7 @@ packages:
dev: false
file:projects/server-tool.tgz:
resolution: {integrity: sha512-4LbvIbvlIpS89Azl2cVRzaJheqQxwnBISelhQv16paTJ6P14YRbr9Af76BDqFUXURo2Ze/dyyn4wH9n47OMNJw==, tarball: file:projects/server-tool.tgz}
resolution: {integrity: sha512-VxPpII/+/u5gj40wFyU/wiV9EevgwadwNF3FFZGsnG4SJmWf34aSvvH4Cz8WilEAzeA4LXlUS20stY+9hFFycg==, tarball: file:projects/server-tool.tgz}
name: '@rush-temp/server-tool'
version: 0.0.0
dependencies:
@ -14925,7 +14949,7 @@ packages:
dev: false
file:projects/server-tracker-resources.tgz:
resolution: {integrity: sha512-12vsorCEm5CJLA5wLdI+6cZxtVT4rbCrJLPTVGPM9UIO6SxZRgYUde+6WWMXVUrReAlnuq8VV/G6dIOXkjiNIg==, tarball: file:projects/server-tracker-resources.tgz}
resolution: {integrity: sha512-vjwoWiXZ7Zp6yt6qzcJWA9SWl9wSWbL7Rrvha9BGksJltWnRElUKF46bJ2w6UbI/x8xiuqhyrs1ao/JqkE12nA==, tarball: file:projects/server-tracker-resources.tgz}
name: '@rush-temp/server-tracker-resources'
version: 0.0.0
dependencies:
@ -15042,7 +15066,7 @@ packages:
dev: false
file:projects/setting-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-eAAOQAq+VNDEMWirKNp9bEfNSG6QioFNvOSJv5Cb5LAPOFmOvwXfohJpu2SpQxqSblKA6vIpJ3Md+7VXuKKCEw==, tarball: file:projects/setting-resources.tgz}
resolution: {integrity: sha512-P+IXGteZUB8of/aZfG5kPBk2X6/mnjBXJ2wHPBqEoeCQ4E6zG0vGuLepchPsIglC0c31otUJ8hdKvUnAtR1LHw==, tarball: file:projects/setting-resources.tgz}
id: file:projects/setting-resources.tgz
name: '@rush-temp/setting-resources'
version: 0.0.0
@ -15196,7 +15220,7 @@ packages:
dev: false
file:projects/task-resources.tgz_5536a2d3219f8677582bfc0330dae14a:
resolution: {integrity: sha512-mv08VBqVcmiX3RPPWrnXOFNRVy0cig+oZ+veREZ7TQ9nd1famDje4fHsKoWdU6+IO/OICPEOZ4whxo9KjEBCeQ==, tarball: file:projects/task-resources.tgz}
resolution: {integrity: sha512-5D40zL2OmfT/AUhLntguTpyNU69aI7rPa1LY3gc/ECo36wS0sy3KMpCWKSHo1bPmhOjTYnFLLN/Q8osYPZC8qw==, tarball: file:projects/task-resources.tgz}
id: file:projects/task-resources.tgz
name: '@rush-temp/task-resources'
version: 0.0.0
@ -15231,7 +15255,7 @@ packages:
dev: false
file:projects/task.tgz:
resolution: {integrity: sha512-vuIDzbmNkJCJOIgGk5kKyYMdX+b9SWj6waIdaauTu56LU3+eqzPCmVCF+DB+0ScKH4m8WpagsXbSAesyy/GrzQ==, tarball: file:projects/task.tgz}
resolution: {integrity: sha512-gELHSUNsbaoFb7WSNRwJvbGwL1cZdxrEV8CKNnxgYQY57q4aUuA0GhDcB708C28w9ZO+oMEWgHyv6rDEybT76g==, tarball: file:projects/task.tgz}
name: '@rush-temp/task'
version: 0.0.0
dependencies:
@ -15273,7 +15297,7 @@ packages:
dev: false
file:projects/telegram-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-JgI/RYlXUX9pmPE485FJB9YH9UCoPcM2DLOvpPM9G2WXYgt4uinesyC+q5DNyisqARB6es5DgZaeNuVVF3t1Ng==, tarball: file:projects/telegram-resources.tgz}
resolution: {integrity: sha512-vkLKhAlKuBp79AlHPC6V9PZ6w0vg8Yt4hXU85BwU7ClVX3eSDEl+98DuWRw3b57oBNnPx3g9ou22XHSO+wOb3Q==, tarball: file:projects/telegram-resources.tgz}
id: file:projects/telegram-resources.tgz
name: '@rush-temp/telegram-resources'
version: 0.0.0
@ -15308,7 +15332,7 @@ packages:
dev: false
file:projects/telegram.tgz:
resolution: {integrity: sha512-+rjDzAtro+awqj9URYLk9VfjjEOuheHNMPpvfK1Lzf76bn2VpkZEPSdFXn0MAoYI66RQbhfvVYambWV+FGuxBg==, tarball: file:projects/telegram.tgz}
resolution: {integrity: sha512-+8aAsUVL2poPv9BqbOyQnO0iDD57+hzldkVloiZ7WBg+0+772elvhzcK/8umDr6ge8gYUhkdm2RDJSuZuwWpNQ==, tarball: file:projects/telegram.tgz}
name: '@rush-temp/telegram'
version: 0.0.0
dependencies:
@ -15427,7 +15451,7 @@ packages:
dev: false
file:projects/text-editor.tgz_89204ec304a9fe9c91bbfc5394a172bd:
resolution: {integrity: sha512-CUuJVSwdoBUNvSy7GIaxemhr6fLCaU1BrbzzEpecP4HuRfQviyQQ2ME6gj4PWhcYVweFmUa1g3gRrCECtWIU8g==, tarball: file:projects/text-editor.tgz}
resolution: {integrity: sha512-jGIekdEoT82BTFc4UVhbLOuQZJkVp4NSb7P3DOw5JJwJ0KvuU0mQZcA7AGKTI7eAuPBG6xjtFR2f4ykt+QcjmA==, tarball: file:projects/text-editor.tgz}
id: file:projects/text-editor.tgz
name: '@rush-temp/text-editor'
version: 0.0.0
@ -15529,7 +15553,7 @@ packages:
dev: false
file:projects/tool.tgz:
resolution: {integrity: sha512-Ioj5hc4nIdKPXhwXnABbqeWZHk9AppnFv8SZWBY4MQPH2TQTUm1DeY3p20qs8btaCzYeTNYDqaGYaWISGxmLqQ==, tarball: file:projects/tool.tgz}
resolution: {integrity: sha512-lPQdPlcd6ajld4cEO1nodKIc4avevkjqB1qAYMfrLfN9X+F7WWv+bf3r0OLkDpXurUiufMgCo/KAMhypIeLdJw==, tarball: file:projects/tool.tgz}
name: '@rush-temp/tool'
version: 0.0.0
dependencies:
@ -15597,7 +15621,7 @@ packages:
dev: false
file:projects/tracker-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-lo7E5NfHbF6MhcCtZQxIIyL7Y0Ekx6Uw5x66IBv6xJy51KuLrIhgXDvkz+1o/rB+iTVzkVlJvKr/7VNOFz7agQ==, tarball: file:projects/tracker-resources.tgz}
resolution: {integrity: sha512-mAMoLXcGbvCBFiy0w3I2qDd//xQHlzzNsjkDY9h/xREiAzkdzgusZNu6J/tlYEK0gJGKKXki46a1sH9dBNgIzQ==, tarball: file:projects/tracker-resources.tgz}
id: file:projects/tracker-resources.tgz
name: '@rush-temp/tracker-resources'
version: 0.0.0
@ -15633,7 +15657,7 @@ packages:
dev: false
file:projects/tracker.tgz:
resolution: {integrity: sha512-EI6jw1l4ymzG7wWjy1i2h6YGREq+MZmJ4lccKzARPncmuniPu705pcyZJNyt2JaI8buyMBs5MGMWZfqhaiEkHg==, tarball: file:projects/tracker.tgz}
resolution: {integrity: sha512-nvbByvB71GEtdBV8AKozLay8li3ur+dd4BGncUFHzBhQ2eBt1ys6NTMYMbYrnqgxd2428MewMloSuBzo5aph9g==, tarball: file:projects/tracker.tgz}
name: '@rush-temp/tracker'
version: 0.0.0
dependencies:
@ -15713,7 +15737,7 @@ packages:
dev: false
file:projects/view-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-/ZeEjUOaNbJrTki3A99XO4dPaGS2ppXY5RClZJg8gbF4HVwcQG8PtglqJkc0ZWXzhAYUwYyM8sXEKQA9KJgtWw==, tarball: file:projects/view-resources.tgz}
resolution: {integrity: sha512-QAyHumkqjZdhKiWroVQgmOXtsG7G5TKLEVro5hGna94kol1x/9jgl73djPm3Qjnc7zsQJ6A2Rp1WYjrhq0nzSw==, tarball: file:projects/view-resources.tgz}
id: file:projects/view-resources.tgz
name: '@rush-temp/view-resources'
version: 0.0.0
@ -15769,7 +15793,7 @@ packages:
dev: false
file:projects/workbench-assets.tgz:
resolution: {integrity: sha512-qtMH4kaxQh95CWI0kc2UYhHUV4xLjpi+x0I8YvAmieQqPL/S8Mw6B7bNdRX3uDogVgzu4SNjqq96ugSbauGIeQ==, tarball: file:projects/workbench-assets.tgz}
resolution: {integrity: sha512-xWctLqgcvqMWeALmiUfT3/EZMt4G/PhEGy/cDkbUImIHRHP1REVK6wHhiFscUDTKDKZ8RFqp0ADgDhBcZaocfQ==, tarball: file:projects/workbench-assets.tgz}
name: '@rush-temp/workbench-assets'
version: 0.0.0
dependencies:
@ -15790,7 +15814,7 @@ packages:
dev: false
file:projects/workbench-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-ff56NyjSj5PgQmkJbToL25SlAvhBjTKIJ1Zu6cTQuAwoji1/c+K5VC5xNj5lgE+ZD0hVkGqKUHZwmZNK5ti8RA==, tarball: file:projects/workbench-resources.tgz}
resolution: {integrity: sha512-yR3vzKvvDRTboCppCkQH/gvl2AP9hX79KeB9U1Zgh85Iaf23xCz4ayUGuQVN4N/2WBGSP/37WK0vXt+Uu36qqg==, tarball: file:projects/workbench-resources.tgz}
id: file:projects/workbench-resources.tgz
name: '@rush-temp/workbench-resources'
version: 0.0.0

View File

@ -28,6 +28,7 @@
},
"dependencies": {
"@hcengineering/platform": "^0.6.7",
"@hcengineering/core": "^0.6.17",
"@hcengineering/server-token": "~0.6.0"
}
}

View File

@ -15,8 +15,9 @@
//
import type { Request, Response } from '@hcengineering/platform'
import platform, { Status, Severity } from '@hcengineering/platform'
import platform, { Severity, Status } from '@hcengineering/platform'
import { getWorkspaceId } from '@hcengineering/core'
import { generateToken } from '@hcengineering/server-token'
interface LoginInfo {
@ -37,7 +38,7 @@ function login (endpoint: string, email: string, password: string, workspace: st
return { error: new Status(Severity.ERROR, platform.status.Unauthorized, {}) }
}
const token = generateToken(email, workspace)
const token = generateToken(email, getWorkspaceId(workspace))
return { result: { token, endpoint } }
}

View File

@ -23,6 +23,7 @@ import {
DOMAIN_TX,
FindOptions,
FindResult,
getWorkspaceId,
MeasureMetricsContext,
Ref,
ServerStorage,
@ -122,7 +123,7 @@ export async function connect (handler: (tx: Tx) => void): Promise<ClientConnect
factory: createNullFullTextAdapter,
url: ''
},
workspace: ''
workspace: getWorkspaceId('')
}
const serverStorage = await createServerStorage(conf)
setMetadata(devmodel.metadata.DevModel, serverStorage)

View File

@ -31,8 +31,7 @@
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5",
"@types/ws": "^8.5.3",
"@types/faker": "~5.5.9",
"@types/minio": "~7.0.11"
"@types/faker": "~5.5.9"
},
"dependencies": {
"commander": "^8.1.0",
@ -53,10 +52,10 @@
"@hcengineering/chunter": "~0.6.1",
"pdfkit": "~0.13.0",
"@hcengineering/attachment": "~0.6.1",
"minio": "^7.0.26",
"@types/pdfkit": "~0.12.3",
"@hcengineering/task": "~0.6.0",
"jpeg-js": "~0.4.3",
"@hcengineering/server-token": "~0.6.0"
"@hcengineering/server-token": "~0.6.0",
"@hcengineering/minio": "^0.6.0"
}
}

View File

@ -1,7 +1,7 @@
import attachment, { Attachment } from '@hcengineering/attachment'
import { Class, Doc, generateId, Ref, Space, TxOperations } from '@hcengineering/core'
import { Class, Doc, generateId, Ref, Space, TxOperations, WorkspaceId } from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import faker from 'faker'
import { Client } from 'minio'
import PDFDocument from 'pdfkit'
export interface AttachmentOptions {
@ -13,8 +13,8 @@ export interface AttachmentOptions {
export async function addAttachments<T extends Doc> (
options: AttachmentOptions,
client: TxOperations,
minio: Client,
dbName: string,
minio: MinioService,
workspaceId: WorkspaceId,
space: Ref<Space>,
objectId: Ref<T>,
_class: Ref<Class<T>>,
@ -35,7 +35,7 @@ export async function addAttachments<T extends Doc> (
const buf = doc.read()
bufLen = buf.length
await minio.putObject(dbName, attachmentId, buf, bufLen, { 'Content-Type': 'application/pdf' })
await minio.put(workspaceId, attachmentId, buf, bufLen, { 'Content-Type': 'application/pdf' })
}
await client.addCollection(

View File

@ -1,13 +1,13 @@
import client from '@hcengineering/client'
import clientResources from '@hcengineering/client-resources'
import { Client } from '@hcengineering/core'
import { Client, WorkspaceId } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform'
import { generateToken } from '@hcengineering/server-token'
// eslint-disable-next-line
const WebSocket = require('ws')
export async function connect (transactorUrl: string, workspace: string): Promise<Client> {
export async function connect (transactorUrl: string, workspace: WorkspaceId): Promise<Client> {
console.log('connecting to transactor...')
const token = generateToken('anticrm@hc.engineering', workspace)

View File

@ -15,9 +15,10 @@
//
import { program } from 'commander'
import { Client } from 'minio'
import { MinioService } from '@hcengineering/minio'
import { generateIssues } from './issues'
import { generateContacts } from './recruit'
import { getWorkspaceId } from '@hcengineering/core'
const transactorUrl = process.env.TRANSACTOR_URL
if (transactorUrl === undefined) {
@ -43,7 +44,7 @@ if (minioSecretKey === undefined) {
process.exit(1)
}
const minio = new Client({
const minio = new MinioService({
endPoint: minioEndpoint,
port: 9000,
useSSL: false,
@ -55,16 +56,16 @@ program.version('0.0.1')
// available types: recruit, issue
program
.command('gen <genType> <workspace> <count>')
.command('gen <genType> <workspace> <productId> <count>')
.description('generate a bunch of random candidates with attachemnts and comments or issues')
.option('-r, --random', 'generate random ids. So every call will add count <count> more candidates.', false)
.option('-l, --lite', 'use same pdf and same account for applicant and candidates', false)
.action(async (genType: string, workspace: string, count: number, cmd) => {
.action(async (genType: string, workspace: string, productId: string, count: number, cmd) => {
switch (genType) {
case 'recruit':
return await generateContacts(
transactorUrl,
workspace,
getWorkspaceId(workspace, productId),
{
contacts: count,
random: cmd.random as boolean,
@ -81,7 +82,7 @@ program
minio
)
case 'issue':
return await generateIssues(transactorUrl, workspace, {
return await generateIssues(transactorUrl, getWorkspaceId(workspace, productId), {
count
})
default:

View File

@ -8,7 +8,8 @@ import core, {
AttachedData,
generateId,
Ref,
SortingOrder
SortingOrder,
WorkspaceId
} from '@hcengineering/core'
import tracker, { calcRank, Issue, IssuePriority, IssueStatus } from '../../../plugins/tracker/lib'
@ -41,8 +42,12 @@ export interface IssueOptions {
count: number // how many issues to add
}
export async function generateIssues (transactorUrl: string, dbName: string, options: IssueOptions): Promise<void> {
const connection = await connect(transactorUrl, dbName)
export async function generateIssues (
transactorUrl: string,
workspaceId: WorkspaceId,
options: IssueOptions
): Promise<void> {
const connection = await connect(transactorUrl, workspaceId)
const accounts = await connection.findAll(contact.class.EmployeeAccount, {})
const account = faker.random.arrayElement(accounts)
const client = new TxOperations(connection, account._id)

View File

@ -8,14 +8,15 @@ import core, {
metricsToString,
MixinUpdate,
Ref,
TxOperations
TxOperations,
WorkspaceId
} from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import recruit from '@hcengineering/model-recruit'
import { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import { genRanks, State } from '@hcengineering/task'
import faker from 'faker'
import jpeg, { BufferRet } from 'jpeg-js'
import { Client } from 'minio'
import { addAttachments, AttachmentOptions } from './attachments'
import { addComments, CommentOptions } from './comments'
import { connect } from './connect'
@ -41,11 +42,11 @@ export interface RecruitOptions {
export async function generateContacts (
transactorUrl: string,
dbName: string,
workspaceId: WorkspaceId,
options: RecruitOptions,
minio: Client
minio: MinioService
): Promise<void> {
const connection = await connect(transactorUrl, dbName)
const connection = await connect(transactorUrl, workspaceId)
const accounts = await connection.findAll(contact.class.EmployeeAccount, {})
const accountIds = accounts.map((a) => a._id)
@ -60,12 +61,12 @@ export async function generateContacts (
const ctx = new MeasureMetricsContext('recruit', { contacts: options.contacts })
for (let i = 0; i < options.contacts; i++) {
await ctx.with('candidate', {}, (ctx) => genCandidate(ctx, i, minio, dbName, options, candidates, client))
await ctx.with('candidate', {}, (ctx) => genCandidate(ctx, i, minio, workspaceId, options, candidates, client))
}
// Work on Vacancy/Applications.
for (let i = 0; i < options.vacancy; i++) {
await ctx.with('vacancy', {}, (ctx) =>
genVacansyApplicants(ctx, accountIds, options, i, client, minio, dbName, candidates, emoloyeeIds)
genVacansyApplicants(ctx, accountIds, options, i, client, minio, workspaceId, candidates, emoloyeeIds)
)
}
@ -81,8 +82,8 @@ async function genVacansyApplicants (
options: RecruitOptions,
i: number,
client: TxOperations,
minio: Client,
dbName: string,
minio: MinioService,
workspaceId: WorkspaceId,
candidates: Ref<Candidate>[],
emoloyeeIds: Ref<Employee>[]
): Promise<void> {
@ -112,7 +113,7 @@ async function genVacansyApplicants (
options.attachments,
client,
minio,
dbName,
workspaceId,
vacancyId,
vacancyId,
recruit.class.Vacancy,
@ -133,7 +134,7 @@ async function genVacansyApplicants (
const rankGen = genRanks(candidates.length)
for (const candidateId of applicantsFor) {
await ctx.with('applicant', {}, (ctx) =>
genApplicant(ctx, vacancyId, candidateId, emoloyeeIds, states, client, options, minio, dbName, rankGen)
genApplicant(ctx, vacancyId, candidateId, emoloyeeIds, states, client, options, minio, workspaceId, rankGen)
)
}
}
@ -146,8 +147,8 @@ async function genApplicant (
states: Ref<State>[],
client: TxOperations,
options: RecruitOptions,
minio: Client,
dbName: string,
minio: MinioService,
workspaceId: WorkspaceId,
rankGen: Generator<string, void, unknown>
): Promise<void> {
const applicantId = `vacancy-${vacancyId}-${candidateId}` as Ref<Applicant>
@ -180,7 +181,7 @@ async function genApplicant (
options.attachments,
client,
minio,
dbName,
workspaceId,
vacancyId,
applicantId,
recruit.class.Applicant,
@ -212,8 +213,8 @@ const liteAvatar = generateAvatar(0)
async function genCandidate (
ctx: MeasureContext,
i: number,
minio: Client,
dbName: string,
minio: MinioService,
workspaceId: WorkspaceId,
options: RecruitOptions,
candidates: Ref<Candidate>[],
client: TxOperations
@ -225,7 +226,7 @@ async function genCandidate (
if (!options.lite) {
await ctx.with('avatar', {}, () =>
minio.putObject(dbName, imgId, jpegImageData.data, jpegImageData.data.length, { 'Content-Type': 'image/jpeg' })
minio.put(workspaceId, imgId, jpegImageData.data, jpegImageData.data.length, { 'Content-Type': 'image/jpeg' })
)
}
const candidate: Data<Person> = {
@ -283,7 +284,7 @@ async function genCandidate (
options.attachments,
client,
minio,
dbName,
workspaceId,
recruit.space.CandidatesPublic,
candidateId,
contact.class.Person,

View File

@ -14,8 +14,7 @@
// limitations under the License.
//
import type { Doc, Ref, TxResult } from '@hcengineering/core'
import { DOMAIN_TX, MeasureMetricsContext } from '@hcengineering/core'
import { Doc, DOMAIN_TX, getWorkspaceId, MeasureMetricsContext, Ref, TxResult } from '@hcengineering/core'
import { createInMemoryTxAdapter } from '@hcengineering/dev-storage'
import {
createInMemoryAdapter,
@ -74,12 +73,13 @@ export async function start (port: number, host?: string): Promise<void> {
factory: createNullFullTextAdapter,
url: ''
},
workspace: ''
workspace: getWorkspaceId('')
}
return createPipeline(conf, [])
},
(token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
port,
'',
host
)
}

View File

@ -23,7 +23,8 @@ import type {
Ref,
StorageIterator,
Tx,
TxResult
TxResult,
WorkspaceId
} from '@hcengineering/core'
import { Hierarchy, TxDb } from '@hcengineering/core'
import builder from '@hcengineering/model-all'
@ -82,7 +83,7 @@ class InMemoryTxAdapter implements TxAdapter {
export async function createInMemoryTxAdapter (
hierarchy: Hierarchy,
url: string,
workspace: string
workspace: WorkspaceId
): Promise<TxAdapter> {
return new InMemoryTxAdapter(hierarchy)
}

View File

@ -43,12 +43,11 @@
"@types/request": "~2.48.8"
},
"dependencies": {
"mongodb": "^4.9.0",
"mongodb": "^4.11.0",
"commander": "^8.1.0",
"@hcengineering/account": "~0.6.0",
"@hcengineering/core": "^0.6.17",
"@hcengineering/contact": "~0.6.8",
"minio": "^7.0.26",
"@hcengineering/model-all": "~0.6.0",
"@hcengineering/model-telegram": "~0.6.0",
"@hcengineering/telegram": "~0.6.2",
@ -112,6 +111,7 @@
"@hcengineering/lead": "~0.6.0",
"email-addresses": "^5.0.0",
"libphonenumber-js": "^1.9.46",
"@hcengineering/setting": "~0.6.1"
"@hcengineering/setting": "~0.6.1",
"@hcengineering/minio": "^0.6.0"
}
}

View File

@ -18,18 +18,50 @@ import { prepareTools as prepareToolsRaw } from '@hcengineering/server-tool'
import { Data, Tx, Version } from '@hcengineering/core'
import { MigrateOperation } from '@hcengineering/model'
import builder, { migrateOperations, version } from '@hcengineering/model-all'
import { Client } from 'minio'
import { MinioService } from '@hcengineering/minio'
import { devTool } from '.'
import { serverAttachmentId } from '@hcengineering/server-attachment'
import { serverCalendarId } from '@hcengineering/server-calendar'
import { serverChunterId } from '@hcengineering/server-chunter'
import { serverContactId } from '@hcengineering/server-contact'
import { serverGmailId } from '@hcengineering/server-gmail'
import { serverInventoryId } from '@hcengineering/server-inventory'
import { serverLeadId } from '@hcengineering/server-lead'
import { serverNotificationId } from '@hcengineering/server-notification'
import { serverRecruitId } from '@hcengineering/server-recruit'
import { serverSettingId } from '@hcengineering/server-setting'
import { serverTagsId } from '@hcengineering/server-tags'
import { serverTaskId } from '@hcengineering/server-task'
import { serverTrackerId } from '@hcengineering/server-tracker'
import { serverTelegramId } from '@hcengineering/server-telegram'
import { serverHrId } from '@hcengineering/server-hr'
import { addLocation } from '@hcengineering/platform'
addLocation(serverAttachmentId, () => import('@hcengineering/server-attachment-resources'))
addLocation(serverContactId, () => import('@hcengineering/server-contact-resources'))
addLocation(serverNotificationId, () => import('@hcengineering/server-notification-resources'))
addLocation(serverChunterId, () => import('@hcengineering/server-chunter-resources'))
addLocation(serverInventoryId, () => import('@hcengineering/server-inventory-resources'))
addLocation(serverLeadId, () => import('@hcengineering/server-lead-resources'))
addLocation(serverRecruitId, () => import('@hcengineering/server-recruit-resources'))
addLocation(serverSettingId, () => import('@hcengineering/server-setting-resources'))
addLocation(serverTaskId, () => import('@hcengineering/server-task-resources'))
addLocation(serverTrackerId, () => import('@hcengineering/server-tracker-resources'))
addLocation(serverTagsId, () => import('@hcengineering/server-tags-resources'))
addLocation(serverCalendarId, () => import('@hcengineering/server-calendar-resources'))
addLocation(serverGmailId, () => import('@hcengineering/server-gmail-resources'))
addLocation(serverTelegramId, () => import('@hcengineering/server-telegram-resources'))
addLocation(serverHrId, () => import('@hcengineering/server-hr-resources'))
function prepareTools (): {
mongodbUri: string
minio: Client
minio: MinioService
txes: Tx[]
version: Data<Version>
migrateOperations: MigrateOperation[]
productId: string
} {
return { ...prepareToolsRaw(builder.getTxes()), version, migrateOperations, productId: '' }
return { ...prepareToolsRaw(builder.getTxes()), version, migrateOperations }
}
devTool(prepareTools)
devTool(prepareTools, '')

View File

@ -14,12 +14,12 @@
//
import contact, { EmployeeAccount } from '@hcengineering/contact'
import { Ref, TxOperations } from '@hcengineering/core'
import { Ref, TxOperations, WorkspaceId } from '@hcengineering/core'
import { connect } from '@hcengineering/server-tool'
import { deepEqual } from 'fast-equals'
export async function removeDuplicates (transactorUrl: string, dbName: string): Promise<void> {
const connection = await connect(transactorUrl, dbName)
export async function removeDuplicates (transactorUrl: string, workspaceId: WorkspaceId): Promise<void> {
const connection = await connect(transactorUrl, workspaceId)
try {
console.log('loading cvs document...')

View File

@ -14,7 +14,15 @@
//
import contact, { Contact, EmployeeAccount, Organization } from '@hcengineering/contact'
import core, { Class, DocumentUpdate, MixinUpdate, Ref, SortingOrder, TxOperations } from '@hcengineering/core'
import core, {
Class,
DocumentUpdate,
MixinUpdate,
Ref,
SortingOrder,
TxOperations,
WorkspaceId
} from '@hcengineering/core'
import lead, { Customer, Funnel, Lead } from '@hcengineering/lead'
import { connect } from '@hcengineering/server-tool'
import task, { calcRank, DoneState, genRanks, State } from '@hcengineering/task'
@ -125,8 +133,8 @@ export async function updateStates<T extends State | DoneState> (
}
}
export async function importLead (transactorUrl: string, dbName: string, csvFile: string): Promise<void> {
const connection = await connect(transactorUrl, dbName)
export async function importLead (transactorUrl: string, workspaceId: WorkspaceId, csvFile: string): Promise<void> {
const connection = await connect(transactorUrl, workspaceId)
try {
console.log('loading cvs document...')

View File

@ -14,7 +14,7 @@
//
import contact, { Contact, EmployeeAccount, Organization } from '@hcengineering/contact'
import core, { generateId, MixinUpdate, Ref, TxOperations } from '@hcengineering/core'
import core, { generateId, MixinUpdate, Ref, TxOperations, WorkspaceId } from '@hcengineering/core'
import lead from '@hcengineering/lead'
import { connect } from '@hcengineering/server-tool'
import { readFile } from 'fs/promises'
@ -98,8 +98,8 @@ export async function parseCSV (csvData: string): Promise<any[]> {
})
}
export async function importLead2 (transactorUrl: string, dbName: string, csvFile: string): Promise<void> {
const connection = await connect(transactorUrl, dbName)
export async function importLead2 (transactorUrl: string, workspaceId: WorkspaceId, csvFile: string): Promise<void> {
const connection = await connect(transactorUrl, workspaceId)
try {
console.log('loading cvs document...')

View File

@ -24,7 +24,8 @@ import core, {
Ref,
SortingOrder,
TxOperations,
WithLookup
WithLookup,
WorkspaceId
} from '@hcengineering/core'
import lead, { Customer, Funnel, Lead } from '@hcengineering/lead'
import { connect } from '@hcengineering/server-tool'
@ -229,8 +230,8 @@ export async function parseCSV (csvData: string): Promise<any[]> {
})
}
export async function importOrgs (transactorUrl: string, dbName: string, csvFile: string): Promise<void> {
const connection = (await connect(transactorUrl, dbName, undefined, {
export async function importOrgs (transactorUrl: string, workspaceId: WorkspaceId, csvFile: string): Promise<void> {
const connection = (await connect(transactorUrl, workspaceId, undefined, {
mode: 'backup'
})) as unknown as Client & BackupClient
try {

View File

@ -31,7 +31,8 @@ import core, {
Ref,
Timestamp,
TxOperations,
WithLookup
WithLookup,
WorkspaceId
} from '@hcengineering/core'
import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import recruit, { Candidate } from '@hcengineering/recruit'
@ -385,11 +386,11 @@ export async function updateTalantClasses (
export async function importTalants (
transactorUrl: string,
dbName: string,
workspaceId: WorkspaceId,
csvFile: string,
rekoniUrl: string
): Promise<void> {
const connection = (await connect(transactorUrl, dbName, undefined, {
const connection = (await connect(transactorUrl, workspaceId, undefined, {
mode: 'backup'
})) as unknown as Client & BackupClient
@ -410,7 +411,13 @@ export async function importTalants (
await createMissingClass(client, locationDetails, getEmbeddedLabel('Language & Relocations'))
await updateTalantClasses(client, records, fieldMapping)
await createTalants(client, filledFields, connection, rekoniUrl, generateToken('anticrm@hc.engineering', dbName))
await createTalants(
client,
filledFields,
connection,
rekoniUrl,
generateToken('anticrm@hc.engineering', workspaceId)
)
} catch (err: any) {
console.error(err)
} finally {

View File

@ -13,6 +13,7 @@
// limitations under the License.
//
import { Client as ElasticClient } from '@elastic/elasticsearch'
import { Attachment } from '@hcengineering/attachment'
import core, {
AttachedDoc,
@ -32,6 +33,7 @@ import core, {
Ref,
ServerStorage,
StorageIterator,
toWorkspaceString,
Tx,
TxCollectionCUD,
TxCreateDoc,
@ -39,16 +41,13 @@ import core, {
TxProcessor,
TxRemoveDoc,
TxResult,
TxUpdateDoc
TxUpdateDoc,
WorkspaceId
} from '@hcengineering/core'
import { createElasticAdapter } from '@hcengineering/elastic'
import { MinioService } from '@hcengineering/minio'
import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment'
import { createMongoAdapter, createMongoTxAdapter } from '@hcengineering/mongo'
import { addLocation } from '@hcengineering/platform'
import { serverAttachmentId } from '@hcengineering/server-attachment'
import { serverCalendarId } from '@hcengineering/server-calendar'
import { serverChunterId } from '@hcengineering/server-chunter'
import { serverContactId } from '@hcengineering/server-contact'
import { createMongoAdapter, createMongoTxAdapter, getWorkspaceDB } from '@hcengineering/mongo'
import {
createServerStorage,
DbAdapter,
@ -58,48 +57,35 @@ import {
IndexedDoc,
TxAdapter
} from '@hcengineering/server-core'
import { serverGmailId } from '@hcengineering/server-gmail'
import { serverInventoryId } from '@hcengineering/server-inventory'
import { serverLeadId } from '@hcengineering/server-lead'
import { serverNotificationId } from '@hcengineering/server-notification'
import { serverRecruitId } from '@hcengineering/server-recruit'
import { serverSettingId } from '@hcengineering/server-setting'
import { serverTagsId } from '@hcengineering/server-tags'
import { serverTaskId } from '@hcengineering/server-task'
import { serverTrackerId } from '@hcengineering/server-tracker'
import { serverTelegramId } from '@hcengineering/server-telegram'
import { serverHrId } from '@hcengineering/server-hr'
import { Client as ElasticClient } from '@elastic/elasticsearch'
import { Client } from 'minio'
import { Db, MongoClient } from 'mongodb'
import { listMinioObjects } from './minio'
export async function rebuildElastic (
mongoUrl: string,
dbName: string,
minio: Client,
workspaceId: WorkspaceId,
minio: MinioService,
elasticUrl: string
): Promise<void> {
await dropElastic(elasticUrl, dbName)
return await restoreElastic(mongoUrl, dbName, minio, elasticUrl)
await dropElastic(elasticUrl, workspaceId)
return await restoreElastic(mongoUrl, workspaceId, minio, elasticUrl)
}
async function dropElastic (elasticUrl: string, dbName: string): Promise<void> {
async function dropElastic (elasticUrl: string, workspaceId: WorkspaceId): Promise<void> {
console.log('drop existing elastic docment')
const client = new ElasticClient({
node: elasticUrl
})
const productWs = toWorkspaceString(workspaceId)
await new Promise((resolve, reject) => {
client.indices.exists(
{
index: dbName
index: productWs
},
(err: any, result: any) => {
if (err != null) reject(err)
if (result.body === true) {
client.indices.delete(
{
index: dbName
index: productWs
},
(err: any, result: any) => {
if (err != null) reject(err)
@ -122,31 +108,21 @@ export class ElasticTool {
db!: Db
fulltext!: FullTextIndex
constructor (readonly mongoUrl: string, readonly dbName: string, readonly minio: Client, readonly elasticUrl: string) {
addLocation(serverAttachmentId, () => import('@hcengineering/server-attachment-resources'))
addLocation(serverContactId, () => import('@hcengineering/server-contact-resources'))
addLocation(serverNotificationId, () => import('@hcengineering/server-notification-resources'))
addLocation(serverChunterId, () => import('@hcengineering/server-chunter-resources'))
addLocation(serverInventoryId, () => import('@hcengineering/server-inventory-resources'))
addLocation(serverLeadId, () => import('@hcengineering/server-lead-resources'))
addLocation(serverRecruitId, () => import('@hcengineering/server-recruit-resources'))
addLocation(serverSettingId, () => import('@hcengineering/server-setting-resources'))
addLocation(serverTaskId, () => import('@hcengineering/server-task-resources'))
addLocation(serverTrackerId, () => import('@hcengineering/server-tracker-resources'))
addLocation(serverTagsId, () => import('@hcengineering/server-tags-resources'))
addLocation(serverCalendarId, () => import('@hcengineering/server-calendar-resources'))
addLocation(serverGmailId, () => import('@hcengineering/server-gmail-resources'))
addLocation(serverTelegramId, () => import('@hcengineering/server-telegram-resources'))
addLocation(serverHrId, () => import('@hcengineering/server-hr-resources'))
constructor (
readonly mongoUrl: string,
readonly workspaceId: WorkspaceId,
readonly minio: MinioService,
readonly elasticUrl: string
) {
this.mongoClient = new MongoClient(mongoUrl)
}
async connect (): Promise<() => Promise<void>> {
await this.mongoClient.connect()
this.db = this.mongoClient.db(this.dbName)
this.elastic = await createElasticAdapter(this.elasticUrl, this.dbName)
this.storage = await createStorage(this.mongoUrl, this.elasticUrl, this.dbName)
this.db = getWorkspaceDB(this.mongoClient, this.workspaceId)
this.elastic = await createElasticAdapter(this.elasticUrl, this.workspaceId)
this.storage = await createStorage(this.mongoUrl, this.elasticUrl, this.workspaceId)
this.fulltext = new FullTextIndex(this.storage.hierarchy, this.elastic, this.storage, true)
return async () => {
@ -160,8 +136,8 @@ export class ElasticTool {
const doc: Attachment | null = await this.db.collection<Attachment>(DOMAIN_ATTACHMENT).findOne({ file: name })
if (doc == null) return
const buffer = await this.readMinioObject(name)
await this.indexAttachmentDoc(doc, buffer)
const buffer = await this.minio.read(this.workspaceId, name)
await this.indexAttachmentDoc(doc, Buffer.concat(buffer))
}
async indexAttachmentDoc (doc: Attachment, buffer: Buffer): Promise<void> {
@ -179,29 +155,15 @@ export class ElasticTool {
await this.elastic.index(indexedDoc)
}
private async readMinioObject (name: string): Promise<Buffer> {
const data = await this.minio.getObject(this.dbName, name)
const chunks: Buffer[] = []
await new Promise<void>((resolve) => {
data.on('readable', () => {
let chunk
while ((chunk = data.read()) !== null) {
const b = chunk as Buffer
chunks.push(b)
}
})
data.on('end', () => {
resolve()
})
})
return Buffer.concat(chunks)
}
}
async function restoreElastic (mongoUrl: string, dbName: string, minio: Client, elasticUrl: string): Promise<void> {
const tool = new ElasticTool(mongoUrl, dbName, minio, elasticUrl)
async function restoreElastic (
mongoUrl: string,
workspaceId: WorkspaceId,
minio: MinioService,
elasticUrl: string
): Promise<void> {
const tool = new ElasticTool(mongoUrl, workspaceId, minio, elasticUrl)
const done = await tool.connect()
try {
const data = await tool.db.collection<Tx>(DOMAIN_TX).find().toArray()
@ -319,8 +281,8 @@ async function restoreElastic (mongoUrl: string, dbName: string, minio: Client,
console.log('replay elastic mixin transactions done', Date.now() - startMixin)
let apos = 0
if (await minio.bucketExists(dbName)) {
const minioObjects = await listMinioObjects(minio, dbName)
if (await minio.exists(workspaceId)) {
const minioObjects = await minio.list(workspaceId)
console.log('reply elastic documents', minioObjects.length)
for (const d of minioObjects) {
apos++
@ -342,7 +304,7 @@ async function restoreElastic (mongoUrl: string, dbName: string, minio: Client,
}
}
async function createStorage (mongoUrl: string, elasticUrl: string, workspace: string): Promise<ServerStorage> {
async function createStorage (mongoUrl: string, elasticUrl: string, workspace: WorkspaceId): Promise<ServerStorage> {
const conf: DbConfiguration = {
domains: {
[DOMAIN_TX]: 'MongoTx'
@ -370,20 +332,20 @@ async function createStorage (mongoUrl: string, elasticUrl: string, workspace: s
async function createMongoReadOnlyAdapter (
hierarchy: Hierarchy,
url: string,
dbName: string,
workspaceId: WorkspaceId,
modelDb: ModelDb
): Promise<DbAdapter> {
const adapter = await createMongoAdapter(hierarchy, url, dbName, modelDb)
const adapter = await createMongoAdapter(hierarchy, url, workspaceId, modelDb)
return new MongoReadOnlyAdapter(adapter)
}
async function createMongoReadOnlyTxAdapter (
hierarchy: Hierarchy,
url: string,
dbName: string,
workspaceId: WorkspaceId,
modelDb: ModelDb
): Promise<TxAdapter> {
const adapter = await createMongoTxAdapter(hierarchy, url, dbName, modelDb)
const adapter = await createMongoTxAdapter(hierarchy, url, workspaceId, modelDb)
return new MongoReadOnlyTxAdapter(adapter)
}

View File

@ -27,8 +27,10 @@ import core, {
SortingOrder,
Space,
TxOperations,
TxResult
TxResult,
WorkspaceId
} from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import recruit from '@hcengineering/model-recruit'
import { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import { connect } from '@hcengineering/server-tool'
@ -37,7 +39,6 @@ import { deepEqual } from 'fast-equals'
import { existsSync } from 'fs'
import { readdir, readFile, stat } from 'fs/promises'
import mime from 'mime-types'
import { Client } from 'minio'
import { dirname, join } from 'path'
import { parseStringPromise } from 'xml2js'
import { ElasticTool } from './elastic'
@ -90,15 +91,15 @@ function get (data: any, key: string): string | undefined {
export async function importXml (
transactorUrl: string,
dbName: string,
minio: Client,
workspaceId: WorkspaceId,
minio: MinioService,
xmlFile: string,
mongoUrl: string,
elasticUrl: string
): Promise<void> {
const connection = await connect(transactorUrl, dbName)
const connection = await connect(transactorUrl, workspaceId)
const tool = new ElasticTool(mongoUrl, dbName, minio, elasticUrl)
const tool = new ElasticTool(mongoUrl, workspaceId, minio, elasticUrl)
const done = await tool.connect()
try {
console.log('loading xml document...')
@ -152,10 +153,10 @@ export async function importXml (
const fileName = join(candidateRoot, f)
const data = await readFile(fileName)
try {
await minio.statObject(dbName, attachId)
await minio.stat(workspaceId, attachId)
} catch (err: any) {
// No object, put new one.
await minio.putObject(dbName, attachId, data, data.length, {
await minio.put(workspaceId, attachId, data, data.length, {
'Content-Type': type
})
}

View File

@ -54,9 +54,9 @@ import { updateCandidates } from './recruit'
import { clearTelegramHistory } from './telegram'
import { diffWorkspace, dumpWorkspace, restoreWorkspace } from './workspace'
import { Data, Tx, Version } from '@hcengineering/core'
import { Data, getWorkspaceId, Tx, Version } from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import { MigrateOperation } from '@hcengineering/model'
import { Client } from 'minio'
/**
* @public
@ -64,12 +64,12 @@ import { Client } from 'minio'
export function devTool (
prepareTools: () => {
mongodbUri: string
minio: Client
minio: MinioService
txes: Tx[]
version: Data<Version>
migrateOperations: MigrateOperation[]
productId: string
}
},
productId: string
): void {
const serverSecret = process.env.SERVER_SECRET
if (serverSecret === undefined) {
@ -117,7 +117,7 @@ export function devTool (
const { mongodbUri } = prepareTools()
return await withDatabase(mongodbUri, async (db) => {
console.log(`creating account ${cmd.first as string} ${cmd.last as string} (${email})...`)
await createAccount(db, email, cmd.password, cmd.first, cmd.last)
await createAccount(db, productId, email, cmd.password, cmd.first, cmd.last)
})
})
@ -129,7 +129,7 @@ export function devTool (
const { mongodbUri } = prepareTools()
return await withDatabase(mongodbUri, async (db) => {
console.log(`update account ${email} ${cmd.first as string} ${cmd.last as string}...`)
await replacePassword(db, email, cmd.password)
await replacePassword(db, productId, email, cmd.password)
})
})
@ -140,7 +140,7 @@ export function devTool (
const { mongodbUri } = prepareTools()
return await withDatabase(mongodbUri, async (db, client) => {
console.log(`assigning user ${email} to ${workspace}...`)
await assignWorkspace(db, email, workspace)
await assignWorkspace(db, productId, email, workspace)
})
})
@ -160,9 +160,9 @@ export function devTool (
.description('create workspace')
.requiredOption('-o, --organization <organization>', 'organization name')
.action(async (workspace, cmd) => {
const { mongodbUri, txes, version, migrateOperations, productId } = prepareTools()
const { mongodbUri, txes, version, migrateOperations } = prepareTools()
return await withDatabase(mongodbUri, async (db) => {
await createWorkspace(version, txes, migrateOperations, productId, db, workspace, cmd.organization)
await createWorkspace(version, txes, migrateOperations, db, productId, workspace, cmd.organization)
})
})
@ -171,14 +171,14 @@ export function devTool (
.description('set user role')
.action(async (email: string, workspace: string, role: number, cmd) => {
console.log(`set user ${email} role for ${workspace}...`)
await setRole(email, workspace, role)
await setRole(email, workspace, productId, role)
})
program
.command('upgrade-workspace <name>')
.description('upgrade workspace')
.action(async (workspace, cmd) => {
const { mongodbUri, version, txes, migrateOperations, productId } = prepareTools()
const { mongodbUri, version, txes, migrateOperations } = prepareTools()
return await withDatabase(mongodbUri, async (db) => {
await upgradeWorkspace(version, txes, migrateOperations, productId, db, workspace)
})
@ -188,7 +188,7 @@ export function devTool (
.command('upgrade')
.description('upgrade')
.action(async (cmd) => {
const { mongodbUri, version, txes, migrateOperations, productId } = prepareTools()
const { mongodbUri, version, txes, migrateOperations } = prepareTools()
return await withDatabase(mongodbUri, async (db) => {
const workspaces = await listWorkspaces(db, productId)
for (const ws of workspaces) {
@ -204,12 +204,12 @@ export function devTool (
.action(async (workspace, cmd) => {
const { mongodbUri } = prepareTools()
return await withDatabase(mongodbUri, async (db) => {
const ws = await getWorkspace(db, workspace)
const ws = await getWorkspace(db, productId, workspace)
if (ws === null) {
console.log('no workspace exists')
return
}
await dropWorkspace(db, workspace)
await dropWorkspace(db, productId, workspace)
})
})
@ -217,7 +217,7 @@ export function devTool (
.command('list-workspaces')
.description('List workspaces')
.action(async () => {
const { mongodbUri, version, productId } = prepareTools()
const { mongodbUri, version } = prepareTools()
return await withDatabase(mongodbUri, async (db) => {
const workspacesJSON = JSON.stringify(await listWorkspaces(db, productId), null, 2)
console.info(workspacesJSON)
@ -240,27 +240,27 @@ export function devTool (
program
.command('drop-account <name>')
.description('drop account')
.action(async (email, cmd) => {
.action(async (email: string, cmd) => {
const { mongodbUri } = prepareTools()
return await withDatabase(mongodbUri, async (db) => {
await dropAccount(db, email)
await dropAccount(db, productId, email)
})
})
program
.command('dump-workspace <workspace> <dirName>')
.description('dump workspace transactions and minio resources')
.action(async (workspace, dirName, cmd) => {
.action(async (workspace: string, dirName: string, cmd) => {
const { mongodbUri, minio } = prepareTools()
return await dumpWorkspace(mongodbUri, workspace, dirName, minio)
return await dumpWorkspace(mongodbUri, getWorkspaceId(workspace, productId), dirName, minio)
})
program
.command('backup <dirName> <workspace>')
.description('dump workspace transactions and minio resources')
.action(async (dirName, workspace, cmd) => {
.action(async (dirName: string, workspace: string, cmd) => {
const storage = await createFileBackupStorage(dirName)
return await backup(transactorUrl, workspace, storage)
return await backup(transactorUrl, getWorkspaceId(workspace, productId), storage)
})
program
@ -274,7 +274,7 @@ export function devTool (
program
.command('backup-list <dirName>')
.description('list snaphost ids for backup')
.action(async (dirName, cmd) => {
.action(async (dirName: string, cmd) => {
const storage = await createFileBackupStorage(dirName)
return await backupList(storage)
})
@ -282,36 +282,39 @@ export function devTool (
program
.command('backup-s3 <bucketName> <dirName> <workspace>')
.description('dump workspace transactions and minio resources')
.action(async (bucketName, dirName, workspace, cmd) => {
.action(async (bucketName: string, dirName: string, workspace: string, cmd) => {
const { minio } = prepareTools()
const storage = await createMinioBackupStorage(minio, bucketName, dirName)
return await backup(transactorUrl, workspace, storage)
const wsId = getWorkspaceId(workspace, productId)
const storage = await createMinioBackupStorage(minio, wsId, dirName)
return await backup(transactorUrl, wsId, storage)
})
program
.command('backup-s3-restore <bucketName>, <dirName> <workspace> [date]')
.description('dump workspace transactions and minio resources')
.action(async (bucketName, dirName, workspace, date, cmd) => {
.action(async (bucketName: string, dirName: string, workspace: string, date, cmd) => {
const { minio } = prepareTools()
const storage = await createMinioBackupStorage(minio, bucketName, dirName)
return await restore(transactorUrl, workspace, storage, parseInt(date ?? '-1'))
const wsId = getWorkspaceId(bucketName, productId)
const storage = await createMinioBackupStorage(minio, wsId, dirName)
return await restore(transactorUrl, wsId, storage, parseInt(date ?? '-1'))
})
program
.command('backup-s3-list <bucketName> <dirName>')
.description('list snaphost ids for backup')
.action(async (bucketName, dirName, cmd) => {
.action(async (bucketName: string, dirName: string, cmd) => {
const { minio } = prepareTools()
const storage = await createMinioBackupStorage(minio, bucketName, dirName)
const wsId = getWorkspaceId(bucketName, productId)
const storage = await createMinioBackupStorage(minio, wsId, dirName)
return await backupList(storage)
})
program
.command('restore-workspace <workspace> <dirName>')
.description('restore workspace transactions and minio resources from previous dump.')
.action(async (workspace, dirName, cmd) => {
.action(async (workspace: string, dirName: string, cmd) => {
const { mongodbUri, minio, txes, migrateOperations } = prepareTools()
return await restoreWorkspace(
mongodbUri,
workspace,
getWorkspaceId(workspace, productId),
dirName,
minio,
getElasticUrl(),
@ -324,9 +327,9 @@ export function devTool (
program
.command('diff-workspace <workspace>')
.description('restore workspace transactions and minio resources from previous dump.')
.action(async (workspace, cmd) => {
.action(async (workspace: string, cmd) => {
const { mongodbUri, txes } = prepareTools()
return await diffWorkspace(mongodbUri, workspace, txes)
return await diffWorkspace(mongodbUri, getWorkspaceId(workspace, productId), txes)
})
program
@ -343,7 +346,7 @@ export function devTool (
}
console.log(`clearing ${workspace} history:`)
await clearTelegramHistory(mongodbUri, workspace, telegramDB, minio)
await clearTelegramHistory(mongodbUri, getWorkspaceId(workspace, productId), telegramDB, minio)
})
})
@ -351,7 +354,7 @@ export function devTool (
.command('clear-telegram-all-history')
.description('clear telegram history')
.action(async (cmd) => {
const { mongodbUri, minio, productId } = prepareTools()
const { mongodbUri, minio } = prepareTools()
return await withDatabase(mongodbUri, async (db) => {
const telegramDB = process.env.TELEGRAM_DATABASE
if (telegramDB === undefined) {
@ -363,7 +366,7 @@ export function devTool (
for (const w of workspaces) {
console.log(`clearing ${w.workspace} history:`)
await clearTelegramHistory(mongodbUri, w.workspace, telegramDB, minio)
await clearTelegramHistory(mongodbUri, getWorkspaceId(w.workspace, productId), telegramDB, minio)
}
})
})
@ -371,17 +374,17 @@ export function devTool (
program
.command('rebuild-elastic [workspace]')
.description('rebuild elastic index')
.action(async (workspace, cmd) => {
const { mongodbUri, minio, productId } = prepareTools()
.action(async (workspace: string, cmd) => {
const { mongodbUri, minio } = prepareTools()
return await withDatabase(mongodbUri, async (db) => {
if (workspace === undefined) {
const workspaces = await listWorkspaces(db, productId)
for (const w of workspaces) {
await rebuildElastic(mongodbUri, w.workspace, minio, getElasticUrl())
await rebuildElastic(mongodbUri, getWorkspaceId(w.workspace, productId), minio, getElasticUrl())
}
} else {
await rebuildElastic(mongodbUri, workspace, minio, getElasticUrl())
await rebuildElastic(mongodbUri, getWorkspaceId(workspace, productId), minio, getElasticUrl())
console.log('rebuild end')
}
})
@ -390,56 +393,63 @@ export function devTool (
program
.command('import-xml <workspace> <fileName>')
.description('Import Talants.')
.action(async (workspace, fileName, cmd) => {
.action(async (workspace: string, fileName: string, cmd) => {
const { mongodbUri, minio } = prepareTools()
return await importXml(transactorUrl, workspace, minio, fileName, mongodbUri, getElasticUrl())
return await importXml(
transactorUrl,
getWorkspaceId(workspace, productId),
minio,
fileName,
mongodbUri,
getElasticUrl()
)
})
program
.command('import-lead-csv <workspace> <fileName>')
.description('Import LEAD csv customer organizations')
.action(async (workspace, fileName, cmd) => {
return await importLead(transactorUrl, workspace, fileName)
.action(async (workspace: string, fileName: string, cmd) => {
return await importLead(transactorUrl, getWorkspaceId(workspace, productId), fileName)
})
program
.command('import-lead-csv2 <workspace> <fileName>')
.description('Import LEAD csv customer organizations')
.action(async (workspace, fileName, cmd) => {
return await importLead2(transactorUrl, workspace, fileName)
.action(async (workspace: string, fileName: string, cmd) => {
return await importLead2(transactorUrl, getWorkspaceId(workspace, productId), fileName)
})
program
.command('import-talant-csv <workspace> <fileName>')
.description('Import Talant csv')
.action(async (workspace, fileName, cmd) => {
.action(async (workspace: string, fileName: string, cmd) => {
const rekoniUrl = process.env.REKONI_URL
if (rekoniUrl === undefined) {
console.log('Please provide REKONI_URL environment variable')
exit(1)
}
return await importTalants(transactorUrl, workspace, fileName, rekoniUrl)
return await importTalants(transactorUrl, getWorkspaceId(workspace, productId), fileName, rekoniUrl)
})
program
.command('import-org-csv <workspace> <fileName>')
.description('Import Organizations csv')
.action(async (workspace, fileName, cmd) => {
return await importOrgs(transactorUrl, workspace, fileName)
.action(async (workspace: string, fileName: string, cmd) => {
return await importOrgs(transactorUrl, getWorkspaceId(workspace, productId), fileName)
})
program
.command('lead-duplicates <workspace>')
.description('Find and remove duplicate organizations.')
.action(async (workspace, cmd) => {
return await removeDuplicates(transactorUrl, workspace)
.action(async (workspace: string, cmd) => {
return await removeDuplicates(transactorUrl, getWorkspaceId(workspace, productId))
})
program
.command('generate-token <name> <workspace>')
.command('generate-token <name> <workspace> <productId>')
.description('generate token')
.action(async (name, workspace) => {
console.log(generateToken(name, workspace))
.action(async (name: string, workspace: string, productId) => {
console.log(generateToken(name, getWorkspaceId(workspace, productId)))
})
program
.command('decode-token <token>')
@ -450,14 +460,21 @@ export function devTool (
program
.command('update-recruit <workspace>')
.description('process pdf documents inside minio and update resumes with skills, etc.')
.action(async (workspace) => {
.action(async (workspace: string) => {
const rekoniUrl = process.env.REKONI_URL
if (rekoniUrl === undefined) {
console.log('Please provide REKONI_URL environment variable')
exit(1)
}
const { mongodbUri, minio } = prepareTools()
return await updateCandidates(transactorUrl, workspace, minio, mongodbUri, getElasticUrl(), rekoniUrl)
return await updateCandidates(
transactorUrl,
getWorkspaceId(workspace, productId),
minio,
mongodbUri,
getElasticUrl(),
rekoniUrl
)
})
program.parse(process.argv)

View File

@ -1,33 +0,0 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { BucketItem, Client, ItemBucketMetadata } from 'minio'
export type MinioWorkspaceItem = BucketItem & { metaData: ItemBucketMetadata }
export async function listMinioObjects (minio: Client, dbName: string): Promise<MinioWorkspaceItem[]> {
const items: MinioWorkspaceItem[] = []
const list = await minio.listObjects(dbName, undefined, true)
await new Promise((resolve) => {
list.on('data', (data) => {
items.push({ metaData: {}, ...data })
})
list.on('end', () => {
resolve(null)
})
})
return items
}

View File

@ -16,7 +16,8 @@
import { Attachment } from '@hcengineering/attachment'
import contact, { Channel, ChannelProvider, EmployeeAccount } from '@hcengineering/contact'
import { Ref, TxOperations, WithLookup } from '@hcengineering/core'
import { Ref, TxOperations, WithLookup, WorkspaceId } from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import attachment from '@hcengineering/model-attachment'
import recruit from '@hcengineering/model-recruit'
import { Candidate } from '@hcengineering/recruit'
@ -24,11 +25,9 @@ import { ReconiDocument } from '@hcengineering/rekoni'
import { generateToken } from '@hcengineering/server-token'
import { connect } from '@hcengineering/server-tool'
import tags, { findTagCategory } from '@hcengineering/tags'
import { Client } from 'minio'
import got from 'got'
import { ElasticTool } from './elastic'
import { findOrUpdateAttached } from './utils'
import { readMinioData } from './workspace'
export async function recognize (rekoniUrl: string, data: string, token: string): Promise<ReconiDocument | undefined> {
const { body }: { body?: ReconiDocument } = await got.post(rekoniUrl + '/recognize?format=pdf', {
@ -87,18 +86,18 @@ export async function addChannel (
export async function updateCandidates (
transactorUrl: string,
dbName: string,
minio: Client,
workspaceId: WorkspaceId,
minio: MinioService,
mongoUrl: string,
elasticUrl: string,
rekoniUrl: string
): Promise<void> {
const connection = await connect(transactorUrl, dbName)
const connection = await connect(transactorUrl, workspaceId)
const tool = new ElasticTool(mongoUrl, dbName, minio, elasticUrl)
const tool = new ElasticTool(mongoUrl, workspaceId, minio, elasticUrl)
const done = await tool.connect()
const token = generateToken('anticrm@hc.engineering', dbName)
const token = generateToken('anticrm@hc.engineering', workspaceId)
try {
const client = new TxOperations(connection, 'recruit:account:candidate-importer' as Ref<EmployeeAccount>)
@ -115,10 +114,10 @@ export async function updateCandidates (
if (a.type.includes('application/pdf')) {
console.log('processing', c.name, a.name, `(${cpos}, ${candidates.length})`)
try {
const buffer = Buffer.concat(await readMinioData(minio, dbName, a.file)).toString('base64')
const buffer = Buffer.concat(await minio.read(workspaceId, a.file)).toString('base64')
const document = await recognize(rekoniUrl, buffer, token)
if (document !== undefined) {
await updateAvatar(c, document, minio, dbName, client, tool)
await updateAvatar(c, document, minio, workspaceId, client, tool)
// Update candidate values if applicable
if (isUndef(c.city) && document.city !== undefined) {
@ -203,8 +202,8 @@ export async function updateContacts (
async function updateAvatar (
c: WithLookup<Candidate>,
document: ReconiDocument,
minio: Client,
dbName: string,
minio: MinioService,
workspaceId: WorkspaceId,
client: TxOperations,
tool: ElasticTool
): Promise<void> {
@ -221,7 +220,7 @@ async function updateAvatar (
const attachId = `${c._id}.${document.avatarName}` as Ref<Attachment>
// Upload new avatar for candidate
const data = Buffer.from(document.avatar, 'base64')
await minio.putObject(dbName, attachId, data, data.length, {
await minio.put(workspaceId, attachId, data, data.length, {
'Content-Type': document.avatarFormat
})

View File

@ -14,12 +14,13 @@
// limitations under the License.
//
import { DOMAIN_TX, Ref } from '@hcengineering/core'
import { DOMAIN_TX, Ref, WorkspaceId } from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment'
import contact, { DOMAIN_CHANNEL } from '@hcengineering/model-contact'
import { DOMAIN_TELEGRAM } from '@hcengineering/model-telegram'
import { getWorkspaceDB } from '@hcengineering/mongo'
import telegram, { SharedTelegramMessage, SharedTelegramMessages } from '@hcengineering/telegram'
import { Client } from 'minio'
import { Document, MongoClient, UpdateFilter } from 'mongodb'
const LastMessages = 'last-msgs'
@ -29,14 +30,14 @@ const LastMessages = 'last-msgs'
*/
export async function clearTelegramHistory (
mongoUrl: string,
workspace: string,
workspaceId: WorkspaceId,
tgDb: string,
minio: Client
minio: MinioService
): Promise<void> {
const client = new MongoClient(mongoUrl)
try {
await client.connect()
const workspaceDB = client.db(workspace)
const workspaceDB = getWorkspaceDB(client, workspaceId)
const telegramDB = client.db(tgDb)
const sharedMessages = await workspaceDB
@ -89,12 +90,12 @@ export async function clearTelegramHistory (
workspaceDB.collection(DOMAIN_ATTACHMENT).deleteMany({
attachedToClass: telegram.class.Message
}),
minio.removeObjects(workspace, Array.from(attachments))
minio.remove(workspaceId, Array.from(attachments))
])
console.log('clearing telegram service data...')
await telegramDB.collection(LastMessages).deleteMany({
workspace
workspace: workspaceId
})
} finally {
await client.close()

View File

@ -15,17 +15,17 @@
//
import contact from '@hcengineering/contact'
import core, { DOMAIN_TX, Tx } from '@hcengineering/core'
import core, { DOMAIN_TX, Tx, WorkspaceId } from '@hcengineering/core'
import { MinioService, MinioWorkspaceItem } from '@hcengineering/minio'
import { MigrateOperation } from '@hcengineering/model'
import { getWorkspaceDB } from '@hcengineering/mongo'
import { upgradeModel } from '@hcengineering/server-tool'
import { existsSync } from 'fs'
import { mkdir, open, readFile, writeFile } from 'fs/promises'
import { Client } from 'minio'
import { Document, MongoClient } from 'mongodb'
import { join } from 'path'
import { rebuildElastic } from './elastic'
import { generateModelDiff, printDiff } from './mdiff'
import { listMinioObjects, MinioWorkspaceItem } from './minio'
interface CollectionInfo {
name: string
@ -41,11 +41,16 @@ interface WorkspaceInfo {
/**
* @public
*/
export async function dumpWorkspace (mongoUrl: string, dbName: string, fileName: string, minio: Client): Promise<void> {
export async function dumpWorkspace (
mongoUrl: string,
workspaceId: WorkspaceId,
fileName: string,
minio: MinioService
): Promise<void> {
const client = new MongoClient(mongoUrl)
try {
await client.connect()
const db = client.db(dbName)
const db = getWorkspaceDB(client, workspaceId)
console.log('dumping transactions...')
@ -66,19 +71,19 @@ export async function dumpWorkspace (mongoUrl: string, dbName: string, fileName:
}
console.log('Dump minio objects')
if (await minio.bucketExists(dbName)) {
workspaceInfo.minioData.push(...(await listMinioObjects(minio, dbName)))
if (await minio.exists(workspaceId)) {
workspaceInfo.minioData.push(...(await minio.list(workspaceId)))
const minioDbLocation = fileName + '.minio'
if (!existsSync(minioDbLocation)) {
await mkdir(minioDbLocation)
}
for (const d of workspaceInfo.minioData) {
const stat = await minio.statObject(dbName, d.name)
const stat = await minio.stat(workspaceId, d.name)
d.metaData = stat.metaData
const fileHandle = await open(join(minioDbLocation, d.name), 'w')
const chunks: Buffer[] = await readMinioData(minio, dbName, d.name)
const chunks: Buffer[] = await minio.read(workspaceId, d.name)
for (const b of chunks) {
await fileHandle.write(b)
}
@ -92,41 +97,21 @@ export async function dumpWorkspace (mongoUrl: string, dbName: string, fileName:
}
}
export async function readMinioData (minio: Client, dbName: string, name: string): Promise<Buffer[]> {
const data = await minio.getObject(dbName, name)
const chunks: Buffer[] = []
await new Promise((resolve) => {
data.on('readable', () => {
let chunk
while ((chunk = data.read()) !== null) {
const b = chunk as Buffer
chunks.push(b)
}
})
data.on('end', () => {
resolve(null)
})
})
return chunks
}
export async function restoreWorkspace (
mongoUrl: string,
dbName: string,
workspaceId: WorkspaceId,
fileName: string,
minio: Client,
minio: MinioService,
elasticUrl: string,
transactorUrl: string,
rawTxes: Tx[],
migrateOperations: MigrateOperation[]
): Promise<void> {
console.log('Restoring workspace', mongoUrl, dbName, fileName)
console.log('Restoring workspace', mongoUrl, workspaceId, fileName)
const client = new MongoClient(mongoUrl)
try {
await client.connect()
const db = client.db(dbName)
const db = getWorkspaceDB(client, workspaceId)
const workspaceInfo = JSON.parse((await readFile(fileName + '.workspace.json')).toString()) as WorkspaceInfo
@ -148,12 +133,12 @@ export async function restoreWorkspace (
}
}
if (await minio.bucketExists(dbName)) {
const objectNames = (await listMinioObjects(minio, dbName)).map((i) => i.name)
await minio.removeObjects(dbName, objectNames)
await minio.removeBucket(dbName)
if (await minio.exists(workspaceId)) {
const objectNames = (await minio.list(workspaceId)).map((i) => i.name)
await minio.remove(workspaceId, objectNames)
await minio.delete(workspaceId)
}
await minio.makeBucket(dbName, 'k8s')
await minio.make(workspaceId)
const minioDbLocation = fileName + '.minio'
console.log('Restore minio objects', workspaceInfo.minioData.length)
@ -162,7 +147,7 @@ export async function restoreWorkspace (
const file = await open(join(minioDbLocation, d.name), 'r')
const stream = file.createReadStream()
promises.push(
minio.putObject(dbName, d.name, stream, d.size, d.metaData).then(async () => {
minio.put(workspaceId, d.name, stream, d.size, d.metaData).then(async () => {
await file.close()
})
)
@ -172,19 +157,19 @@ export async function restoreWorkspace (
}
}
await upgradeModel(transactorUrl, dbName, rawTxes, migrateOperations)
await upgradeModel(transactorUrl, workspaceId, rawTxes, migrateOperations)
await rebuildElastic(mongoUrl, dbName, minio, elasticUrl)
await rebuildElastic(mongoUrl, workspaceId, minio, elasticUrl)
} finally {
await client.close()
}
}
export async function diffWorkspace (mongoUrl: string, dbName: string, rawTxes: Tx[]): Promise<void> {
export async function diffWorkspace (mongoUrl: string, workspace: WorkspaceId, rawTxes: Tx[]): Promise<void> {
const client = new MongoClient(mongoUrl)
try {
await client.connect()
const db = client.db(dbName)
const db = getWorkspaceDB(client, workspace)
console.log('diffing transactions...')

View File

@ -108,7 +108,8 @@ export default plugin(coreId, {
},
status: {
ObjectNotFound: '' as StatusCode<{ _id: Ref<Doc> }>,
ItemNotFound: '' as StatusCode<{ _id: Ref<Doc>, _localId: string }>
ItemNotFound: '' as StatusCode<{ _id: Ref<Doc>, _localId: string }>,
InvalidProduct: '' as StatusCode<{ productId: string }>
},
version: {
Model: '' as Ref<Version>

View File

@ -76,3 +76,30 @@ export function toFindResult<T extends Doc> (docs: T[], total?: number): FindRes
const length = total ?? docs.length
return Object.assign(docs, { total: length })
}
/**
* @public
*/
export interface WorkspaceId {
name: string
productId: string
}
/**
* @public
*
* Combine workspace with productId, if not equal ''
*/
export function getWorkspaceId (workspace: string, productId: string = ''): WorkspaceId {
return {
name: workspace,
productId
}
}
/**
* @public
*/
export function toWorkspaceString (id: WorkspaceId, sep = '@'): string {
return id.name + (id.productId === '' ? '' : sep + id.productId)
}

View File

@ -64,7 +64,7 @@ export interface AnySvelteComponentWithProps {
export interface Action {
label: IntlString
icon: Asset | AnySvelteComponent
icon?: Asset | AnySvelteComponent
action: (props: any, ev: Event) => Promise<void>
inline?: boolean
link?: string

View File

@ -33,7 +33,7 @@
{
name: 'workspace',
i18n: login.string.Workspace,
rule: /^[0-9a-z#%&^\-@!)(]{3,63}$/,
rule: /^[0-9a-z#%&^\-!)(]{3,63}$/,
ruleDescr: login.string.WorksaceNameRule
}
]

View File

@ -13,7 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Ref } from '@hcengineering/core'
import core, { Class, Doc, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { AttributesBar, getClient, KeyedAttribute } from '@hcengineering/presentation'
import setting, { settingId } from '@hcengineering/setting'
import { Button, getCurrentLocation, Label, navigate } from '@hcengineering/ui'
@ -21,16 +22,18 @@
export let object: Doc
export let _class: Ref<Class<Doc>>
export let to: Ref<Class<Doc>> | undefined
export let to: Ref<Class<Doc>> | undefined = core.class.Doc
export let ignoreKeys: string[] = []
export let allowedCollections: string[] = []
export let readonly = false
export let showLabel: IntlString | undefined = undefined
export let defaultCollapsed = false
const client = getClient()
const hierarchy = client.getHierarchy()
let keys: KeyedAttribute[] = []
let collapsed: boolean = false
let collapsed: boolean = defaultCollapsed
function updateKeys (_class: Ref<Class<Doc>>, ignoreKeys: string[], to: Ref<Class<Doc>> | undefined): void {
const filtredKeys = getFiltredKeys(hierarchy, _class, ignoreKeys, to)
@ -39,9 +42,10 @@
$: updateKeys(_class, ignoreKeys, to)
$: label = hierarchy.getClass(_class).label
$: label = showLabel ?? hierarchy.getClass(_class).label
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="attrbar-header"
class:collapsed

View File

@ -137,33 +137,42 @@
return actions
}
function getWorkspaceItems () {
return workspaces.map((w) => ({
label: getEmbeddedLabel(w.workspace),
action: async () => {
const loginInfo = (await selectWorkspace(w.workspace))[1]
navigateToWorkspace(w.workspace, loginInfo)
},
isSubmenuRightClicking: true,
component: Menu,
props: {
actions: [
{
label: workbench.string.OpenInNewTab,
action: async () => {
const loginInfo = (await selectWorkspace(w.workspace))[1]
function getWorkspaceItems (): Action[] {
return [
...workspaces.map((w) => ({
label: getEmbeddedLabel(w.workspace),
action: async () => {
const loginInfo = (await selectWorkspace(w.workspace))[1]
navigateToWorkspace(w.workspace, loginInfo)
},
isSubmenuRightClicking: true,
component: Menu,
props: {
actions: [
{
label: workbench.string.OpenInNewTab,
action: async () => {
const loginInfo = (await selectWorkspace(w.workspace))[1]
if (!loginInfo) {
return
if (!loginInfo) {
return
}
setLoginInfo(loginInfo)
const url = locationToUrl({ path: [workbenchId, w.workspace] })
window.open(url, '_blank')?.focus()
}
setLoginInfo(loginInfo)
const url = locationToUrl({ path: [workbenchId, w.workspace] })
window.open(url, '_blank')?.focus()
}
}
]
]
}
})),
{
label: getEmbeddedLabel('...'),
action: async () => {
navigate({ path: [login.component.LoginApp, 'selectWorkspace'] })
},
isSubmenuRightClicking: false
}
}))
]
}
let actions: Action[] = []

View File

@ -43,7 +43,7 @@
"@hcengineering/account": "~0.6.0",
"@hcengineering/platform": "^0.6.7",
"@hcengineering/core": "^0.6.17",
"mongodb": "^4.9.0",
"mongodb": "^4.11.0",
"koa": "^2.13.1",
"koa-router": "^10.1.1",
"koa-bodyparser": "^4.3.0",

View File

@ -20,4 +20,4 @@ import { serveAccount } from '.'
const txes = JSON.parse(JSON.stringify(builder.getTxes())) as Tx[]
serveAccount(getMethods(version, txes, migrateOperations, ''))
serveAccount(getMethods(version, txes, migrateOperations))

View File

@ -28,7 +28,7 @@ import { Db, MongoClient } from 'mongodb'
/**
* @public
*/
export function serveAccount (methods: Record<string, AccountMethod>): void {
export function serveAccount (methods: Record<string, AccountMethod>, productId = ''): void {
const ACCOUNT_PORT = parseInt(process.env.ACCOUNT_PORT ?? '3000')
const dbUri = process.env.MONGO_URL
if (dbUri === undefined) {
@ -71,9 +71,9 @@ export function serveAccount (methods: Record<string, AccountMethod>): void {
const token = extractToken(ctx.request.headers)
const request = ctx.request.body as any
const method = (methods as { [key: string]: (db: Db, request: Request<any>, token?: string) => Response<any> })[
request.method
]
const method = (
methods as { [key: string]: (db: Db, productId: string, request: Request<any>, token?: string) => Response<any> }
)[request.method]
if (method === undefined) {
const response: Response<void> = {
id: request.id,
@ -87,7 +87,7 @@ export function serveAccount (methods: Record<string, AccountMethod>): void {
client = await MongoClient.connect(dbUri)
}
const db = client.db(ACCOUNT_DB)
const result = await method(db, request, token)
const result = await method(db, productId, request, token)
console.log(result)
ctx.body = result
})

View File

@ -33,12 +33,11 @@
"prettier": "^2.7.1",
"ts-node": "^10.8.0",
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5",
"@types/minio": "~7.0.11"
"typescript": "^4.3.5"
},
"dependencies": {
"@hcengineering/platform": "^0.6.7",
"mongodb": "^4.9.0",
"mongodb": "^4.11.0",
"@hcengineering/server-tool": "~0.6.0",
"@hcengineering/server-token": "~0.6.0",
"@hcengineering/client": "^0.6.3",
@ -47,6 +46,6 @@
"dotenv": "~16.0.0",
"got": "^11.8.3",
"@hcengineering/server-backup": "~0.6.0",
"minio": "^7.0.26"
"@hcengineering/minio": "^0.6.0"
}
}

View File

@ -25,6 +25,8 @@ interface Config {
MinioEndpoint: string
MinioAccessKey: string
MinioSecretKey: string
ProductId: string
}
const envMap: { [key in keyof Config]: string } = {
@ -36,7 +38,8 @@ const envMap: { [key in keyof Config]: string } = {
Interval: 'INTERVAL',
MinioEndpoint: 'MINIO_ENDPOINT',
MinioAccessKey: 'MINIO_ACCESS_KEY',
MinioSecretKey: 'MINIO_SECRET_KEY'
MinioSecretKey: 'MINIO_SECRET_KEY',
ProductId: 'PRODUCT_ID'
}
const required: Array<keyof Config> = [
@ -60,7 +63,8 @@ const config: Config = (() => {
Interval: parseInt(process.env[envMap.Interval] ?? '3600'),
MinioEndpoint: process.env[envMap.MinioEndpoint],
MinioAccessKey: process.env[envMap.MinioAccessKey],
MinioSecretKey: process.env[envMap.MinioSecretKey]
MinioSecretKey: process.env[envMap.MinioSecretKey],
ProductId: process.env[envMap.ProductId] ?? ''
}
const missingEnv = required.filter((key) => params[key] === undefined).map((key) => envMap[key])

View File

@ -13,12 +13,13 @@
// limitations under the License.
//
import { getWorkspaceId } from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import { setMetadata } from '@hcengineering/platform'
import { backup, createMinioBackupStorage } from '@hcengineering/server-backup'
import got from 'got'
import { Client as MinioClient } from 'minio'
import config from './config'
import serverToken from '@hcengineering/server-token'
import got from 'got'
import config from './config'
async function getWorkspaces (): Promise<string[]> {
const { body }: { body: { error?: string, result?: any[] } } = await got.post(config.AccountsURL, {
@ -37,7 +38,7 @@ async function getWorkspaces (): Promise<string[]> {
}
export class PlatformWorker {
minio?: MinioClient
minio!: MinioService
async close (): Promise<void> {}
@ -51,7 +52,7 @@ export class PlatformWorker {
minioPort = parseInt(sp[1])
}
this.minio = new MinioClient({
this.minio = new MinioService({
endPoint: minioEndpoint,
port: minioPort,
useSSL: false,
@ -78,8 +79,8 @@ export class PlatformWorker {
for (const ws of workspaces) {
console.log('\n\nBACKUP WORKSPACE ', ws)
try {
const storage = await createMinioBackupStorage(this.minio as MinioClient, 'backups', ws)
await backup(config.TransactorURL, ws, storage)
const storage = await createMinioBackupStorage(this.minio, getWorkspaceId('backups', config.ProductId), ws)
await backup(config.TransactorURL, getWorkspaceId(ws, config.ProductId), storage)
} catch (err: any) {
console.error('\n\nFAILED to BACKUP', ws, err)
}

View File

@ -34,8 +34,7 @@
"ts-node": "^10.8.0",
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5",
"@types/ws": "^8.5.3",
"@types/minio": "~7.0.11"
"@types/ws": "^8.5.3"
},
"dependencies": {
"@hcengineering/core": "^0.6.17",
@ -49,6 +48,6 @@
"y-protocols": "~1.0.5",
"ws": "^8.10.0",
"yjs": "^13.5.42",
"minio": "^7.0.26"
"@hcengineering/minio": "^0.6.0"
}
}

View File

@ -19,8 +19,8 @@ import serverToken from '@hcengineering/server-token'
import { metricsContext } from './metrics'
import { start } from './server'
import { MinioService } from '@hcengineering/minio'
import config from './config'
import { Client as MinioClient } from 'minio'
setMetadata(serverToken.metadata.Secret, config.Secret)
@ -32,7 +32,7 @@ if (sp.length > 1) {
minioPort = parseInt(sp[1])
}
const minio = new MinioClient({
const minio = new MinioService({
endPoint: minioEndpoint,
port: minioPort,
useSSL: false,

View File

@ -5,14 +5,14 @@ import { createServer, IncomingMessage } from 'http'
import WebSocket, { WebSocketServer } from 'ws'
import { setupWSConnection } from './yserver'
import { Client as MinioClient } from 'minio'
import { MinioService } from '@hcengineering/minio'
/**
* @public
* @param sessionFactory -
* @param port -
* @param host -
*/
export function start (ctx: MeasureContext, port: number, minio: MinioClient, host?: string): () => void {
export function start (ctx: MeasureContext, port: number, minio: MinioService, host?: string): () => void {
console.log(`starting server on port ${port} ...`)
const wss = new WebSocketServer({

View File

@ -8,8 +8,7 @@ import { createEncoder, length, toUint8Array, writeVarUint, writeVarUint8Array }
import { Token } from '@hcengineering/server-token'
import WebSocket from 'ws'
import { Client as MinioClient } from 'minio'
import { Readable } from 'stream'
import { MinioService } from '@hcengineering/minio'
const wsReadyStateConnecting = 0
const wsReadyStateOpen = 1
@ -27,31 +26,13 @@ export interface YPersistence {
documentId: string,
ydoc: WSSharedDoc,
token: Token,
minio: MinioClient,
minio: MinioService,
initialContentId: string
) => Promise<void>
writeState: (documentId: string, ydoc: WSSharedDoc, token: Token) => Promise<void>
provider: any
}
async function loadData (data: Readable): Promise<Buffer> {
const chunks: Buffer[] = []
await new Promise((resolve) => {
data.on('readable', () => {
let chunk
while ((chunk = data.read()) !== null) {
const b = chunk as Buffer
chunks.push(b)
}
})
data.on('end', () => {
resolve(null)
})
})
return Buffer.concat(chunks)
}
/**
* @public
*/
@ -62,30 +43,27 @@ persistence = {
documentId: string,
ydoc: WSSharedDoc,
token: Token,
minio: MinioClient,
minio: MinioService,
initialContentId: string
): Promise<void> => {
try {
ydoc.minio = minio
let minioDocument: Readable | undefined
let minioDocument: Buffer | undefined
try {
minioDocument = await minio.getObject(token.workspace, documentId)
minioDocument = Buffer.concat(await minio.read(token.workspace, documentId))
} catch (err: any) {
if (initialContentId !== undefined && initialContentId.length > 0) {
minioDocument = await minio.getObject(token.workspace, initialContentId)
minioDocument = Buffer.concat(await minio.read(token.workspace, initialContentId))
}
}
if (minioDocument !== undefined) {
const buffer = await loadData(minioDocument)
if (buffer.length > 0) {
try {
const uint8arr = new Uint8Array(buffer)
applyUpdate(ydoc, uint8arr)
} catch (err) {
console.error(err)
}
if (minioDocument !== undefined && minioDocument.length > 0) {
try {
const uint8arr = new Uint8Array(minioDocument)
applyUpdate(ydoc, uint8arr)
} catch (err) {
console.error(err)
}
}
} catch (err: any) {
@ -97,7 +75,7 @@ persistence = {
const newUpdates = encodeStateAsUpdate(ydoc)
const buffer = Buffer.from(newUpdates.buffer)
await ydoc?.minio?.putObject(token.workspace, documentId, buffer)
await ydoc?.minio?.put(token.workspace, documentId, buffer)
} catch (err: any) {
console.error(err)
}
@ -147,7 +125,7 @@ class WSSharedDoc extends Doc {
conns = new Map<WebSocket, ConnectionEntry>()
awareness: Awareness
minio?: MinioClient
minio?: MinioService
/**
* @param {{ added: Array<number>, updated: Array<number>, removed: Array<number> }} changes
@ -201,7 +179,7 @@ export function getYDoc (
docId: string,
token: Token,
gc = true,
minio: MinioClient,
minio: MinioService,
initialContentId: string
): WSSharedDoc {
let doc = docs.get(docId)
@ -300,7 +278,7 @@ export function setupWSConnection (
req: any,
documentId: string,
token: Token,
minio: MinioClient,
minio: MinioService,
initialContentId: string,
gc = true
): void {

View File

@ -31,7 +31,6 @@
"@types/express-fileupload": "^1.1.7",
"@types/uuid": "^8.3.1",
"@types/cors": "^2.8.12",
"@types/minio": "~7.0.11",
"esbuild": "^0.15.13",
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9",
@ -55,7 +54,6 @@
"@hcengineering/server-token": "~0.6.0",
"@hcengineering/attachment": "~0.6.1",
"@hcengineering/contrib": "~0.6.0",
"minio": "^7.0.26",
"body-parser": "~1.19.1",
"compression": "~1.7.4",
"sharp": "~0.30.7"

View File

@ -33,8 +33,7 @@
"eslint-config-standard-with-typescript": "^23.0.0",
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5",
"@types/minio": "~7.0.11"
"typescript": "^4.3.5"
},
"dependencies": {
"@hcengineering/core": "^0.6.17",
@ -47,7 +46,6 @@
"@hcengineering/mongo": "~0.6.1",
"@hcengineering/elastic": "~0.6.0",
"elastic-apm-node": "~3.26.0",
"minio": "^7.0.26",
"@hcengineering/server-contact": "~0.6.1",
"@hcengineering/server-contact-resources": "~0.6.0",
"@hcengineering/server-notification": "^0.6.0",
@ -78,6 +76,7 @@
"@hcengineering/server-hr": "~0.6.0",
"@hcengineering/server-hr-resources": "~0.6.0",
"@hcengineering/server-token": "~0.6.0",
"@hcengineering/middleware": "~0.6.0"
"@hcengineering/middleware": "~0.6.0",
"@hcengineering/minio": "^0.6.0"
}
}

View File

@ -67,7 +67,7 @@ setMetadata(serverToken.metadata.Secret, serverSecret)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
console.log(`starting server on ${serverPort}`)
const shutdown = start(url, elasticUrl, minioConf, serverPort)
const shutdown = start(url, elasticUrl, minioConf, serverPort, '')
const close = (): void => {
console.trace('Exiting from server')

View File

@ -13,7 +13,14 @@
// limitations under the License.
//
import { DOMAIN_BLOB, DOMAIN_FULLTEXT_BLOB, DOMAIN_MODEL, DOMAIN_TRANSIENT, DOMAIN_TX } from '@hcengineering/core'
import {
DOMAIN_BLOB,
DOMAIN_FULLTEXT_BLOB,
DOMAIN_MODEL,
DOMAIN_TRANSIENT,
DOMAIN_TX,
WorkspaceId
} from '@hcengineering/core'
import { createElasticAdapter, createElasticBackupDataAdapter } from '@hcengineering/elastic'
import { ModifiedMiddleware, PrivateMiddleware } from '@hcengineering/middleware'
import { createMongoAdapter, createMongoTxAdapter } from '@hcengineering/mongo'
@ -49,7 +56,7 @@ import { serverTelegramId } from '@hcengineering/server-telegram'
import { Token } from '@hcengineering/server-token'
import { serverTrackerId } from '@hcengineering/server-tracker'
import { BroadcastCall, ClientSession, start as startJsonRpc } from '@hcengineering/server-ws'
import { Client as MinioClient } from 'minio'
import { MinioService } from '@hcengineering/minio'
/**
* @public
@ -59,6 +66,7 @@ export function start (
fullTextUrl: string,
minioConf: MinioConfig,
port: number,
productId: string,
host?: string
): () => void {
addLocation(serverAttachmentId, () => import('@hcengineering/server-attachment-resources'))
@ -81,7 +89,7 @@ export function start (
return startJsonRpc(
getMetricsContext(),
(workspace: string) => {
(workspace: WorkspaceId) => {
const conf: DbConfiguration = {
domains: {
[DOMAIN_TX]: 'MongoTx',
@ -122,7 +130,7 @@ export function start (
url: fullTextUrl
},
storageFactory: () =>
new MinioClient({
new MinioService({
...minioConf,
port: 9000,
useSSL: false
@ -138,6 +146,7 @@ export function start (
return new ClientSession(broadcast, token, pipeline)
},
port,
productId,
host
)
}

View File

@ -1408,6 +1408,11 @@
"packageName": "@hcengineering/apm",
"projectFolder": "tools/apm",
"shouldPublish": false
},
{
"packageName": "@hcengineering/minio",
"projectFolder": "server/minio",
"shouldPublish": true
}
]
}

View File

@ -74,7 +74,7 @@ export async function OnAttachmentDelete (
})
storageFx(async (adapter, bucket) => {
await adapter.removeObject(bucket, attach.file)
await adapter.remove(bucket, [attach.file])
})
return []

View File

@ -41,7 +41,7 @@ import { workbenchId } from '@hcengineering/workbench'
export async function channelHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const channel = doc as ChunterSpace
const front = getMetadata(login.metadata.FrontUrl) ?? ''
return `<a href="${front}/${workbenchId}/${control.workspace}/${chunterId}/${channel._id}">${channel.name}</a>`
return `<a href="${front}/${workbenchId}/${control.workspace.name}/${chunterId}/${channel._id}">${channel.name}</a>`
}
/**

View File

@ -32,6 +32,7 @@
"@hcengineering/contact": "~0.6.8",
"@hcengineering/view": "^0.6.1",
"@hcengineering/login": "~0.6.1",
"@hcengineering/workbench": "~0.6.2"
"@hcengineering/workbench": "~0.6.2",
"@hcengineering/minio": "^0.6.0"
}
}

View File

@ -14,13 +14,13 @@
// limitations under the License.
//
import core, { Doc, Tx, TxCreateDoc, TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core'
import type { TriggerControl, MinioClient, BucketItem } from '@hcengineering/server-core'
import contact, { Contact, contactId, formatName, Organization, Person } from '@hcengineering/contact'
import { getMetadata } from '@hcengineering/platform'
import core, { Doc, Tx, TxCreateDoc, TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core'
import login from '@hcengineering/login'
import { workbenchId } from '@hcengineering/workbench'
import { getMetadata } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core'
import view from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
/**
* @public
@ -51,13 +51,14 @@ export async function OnContactDelete (tx: Tx, { findAll, hierarchy, storageFx }
}
storageFx(async (adapter, bucket) => {
await adapter.removeObject(bucket, avatar)
await adapter.remove(bucket, [avatar])
const extra = await listMinioObjects(adapter, bucket, avatar)
if (extra.size > 0) {
for (const e of extra.entries()) {
await adapter.removeObject(bucket, e[1].name)
}
const extra = await adapter.list(bucket, avatar)
if (extra.length > 0) {
await adapter.remove(
bucket,
Array.from(extra.entries()).map((it) => it[1].name)
)
}
})
@ -70,9 +71,9 @@ export async function OnContactDelete (tx: Tx, { findAll, hierarchy, storageFx }
export function personHTMLPresenter (doc: Doc, control: TriggerControl): string {
const person = doc as Person
const front = getMetadata(login.metadata.FrontUrl) ?? ''
return `<a href="${front}/${workbenchId}/${control.workspace}/${contactId}#${view.component.EditDoc}|${person._id}|${
person._class
}">${formatName(person.name)}</a>`
return `<a href="${front}/${workbenchId}/${control.workspace.name}/${contactId}#${view.component.EditDoc}|${
person._id
}|${person._class}">${formatName(person.name)}</a>`
}
/**
@ -89,7 +90,7 @@ export function personTextPresenter (doc: Doc): string {
export function organizationHTMLPresenter (doc: Doc, control: TriggerControl): string {
const organization = doc as Organization
const front = getMetadata(login.metadata.FrontUrl) ?? ''
return `<a href="${front}/${workbenchId}/${control.workspace}/${contactId}#${view.component.EditDoc}|${organization._id}|${organization._class}">${organization.name}</a>`
return `<a href="${front}/${workbenchId}/${control.workspace.name}/${contactId}#${view.component.EditDoc}|${organization._id}|${organization._class}">${organization.name}</a>`
}
/**
@ -112,17 +113,3 @@ export default async () => ({
OrganizationTextPresenter: organizationTextPresenter
}
})
async function listMinioObjects (client: MinioClient, db: string, prefix: string): Promise<Map<string, BucketItem>> {
const items = new Map<string, BucketItem>()
const list = await client.listObjects(db, prefix, true)
await new Promise((resolve) => {
list.on('data', (data) => {
items.set(data.name, { ...data })
})
list.on('end', () => {
resolve(null)
})
})
return items
}

View File

@ -27,7 +27,7 @@ import { workbenchId } from '@hcengineering/workbench'
export async function productHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const product = doc as Product
const front = getMetadata(login.metadata.FrontUrl) ?? ''
return `<a href="${front}/${workbenchId}/${control.workspace}/${inventoryId}/Products/#${view.component.EditDoc}|${product._id}|${product._class}">${product.name}</a>`
return `<a href="${front}/${workbenchId}/${control.workspace.name}/${inventoryId}/Products/#${view.component.EditDoc}|${product._id}|${product._class}">${product.name}</a>`
}
/**

View File

@ -37,7 +37,7 @@ import { addAssigneeNotification } from '@hcengineering/server-task-resources'
export async function leadHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const lead = doc as Lead
const front = getMetadata(login.metadata.FrontUrl) ?? ''
return `<a href="${front}/${workbenchId}/${control.workspace}/${leadId}/${lead.space}/#${view.component.EditDoc}|${lead._id}|${lead._class}">${lead.title}</a>`
return `<a href="${front}/${workbenchId}/${control.workspace.name}/${leadId}/${lead.space}/#${view.component.EditDoc}|${lead._id}|${lead._class}">${lead.title}</a>`
}
/**

View File

@ -39,7 +39,7 @@ import { workbenchId } from '@hcengineering/workbench'
export async function vacancyHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const vacancy = doc as Vacancy
const front = getMetadata(login.metadata.FrontUrl) ?? ''
return `<a href="${front}/${workbenchId}/${control.workspace}/${recruitId}/${vacancy._id}/#${recruit.component.EditVacancy}|${vacancy._id}|${vacancy._class}">${vacancy.name}</a>`
return `<a href="${front}/${workbenchId}/${control.workspace.name}/${recruitId}/${vacancy._id}/#${recruit.component.EditVacancy}|${vacancy._id}|${vacancy._class}">${vacancy.name}</a>`
}
/**
@ -56,7 +56,7 @@ export async function vacancyTextPresenter (doc: Doc): Promise<string> {
export async function applicationHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const applicant = doc as Applicant
const front = getMetadata(login.metadata.FrontUrl) ?? ''
return `<a href="${front}/${workbenchId}/${control.workspace}/${recruitId}/${applicant.space}/#${view.component.EditDoc}|${applicant._id}|${applicant._class}">APP-${applicant.number}</a>`
return `<a href="${front}/${workbenchId}/${control.workspace.name}/${recruitId}/${applicant.space}/#${view.component.EditDoc}|${applicant._id}|${applicant._class}">APP-${applicant.number}</a>`
}
/**

View File

@ -31,7 +31,7 @@ import { workbenchId } from '@hcengineering/workbench'
export async function issueHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const issue = doc as Issue
const front = getMetadata(login.metadata.FrontUrl) ?? ''
return `<a href="${front}/${workbenchId}/${control.workspace}/${taskId}/${issue.space}/#${view.component.EditDoc}|${issue._id}|${issue._class}">Task-${issue.number}</a>`
return `<a href="${front}/${workbenchId}/${control.workspace.name}/${taskId}/${issue.space}/#${view.component.EditDoc}|${issue._id}|${issue._class}">Task-${issue.number}</a>`
}
/**

View File

@ -59,7 +59,7 @@ export async function issueHTMLPresenter (doc: Doc, control: TriggerControl): Pr
const issueName = `${team?.identifier ?? '?'}-${issue.number}`
const front = getMetadata(login.metadata.FrontUrl) ?? ''
return `<a href="${front}/${workbenchId}/${control.workspace}/${trackerId}/${issue.space}/#${tracker.component.EditIssue}|${issue._id}|${issue._class}">${issueName}</a>`
return `<a href="${front}/${workbenchId}/${control.workspace.name}/${trackerId}/${issue.space}/#${tracker.component.EditIssue}|${issue._id}|${issue._class}">${issueName}</a>`
}
/**

View File

@ -24,13 +24,11 @@
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5",
"@types/minio": "~7.0.11",
"@types/ws": "^8.5.3"
},
"dependencies": {
"mongodb": "^4.9.0",
"mongodb": "^4.11.0",
"@hcengineering/platform": "^0.6.7",
"minio": "^7.0.26",
"@hcengineering/core": "^0.6.17",
"@hcengineering/contact": "~0.6.8",
"@hcengineering/client-resources": "~0.6.4",

View File

@ -21,7 +21,7 @@ import { getAccount, getMethods, getWorkspace } from '..'
const DB_NAME = 'test_accounts'
const methods = getMethods(version, builder.getTxes(), migrateOperations, '')
const methods = getMethods(version, builder.getTxes(), migrateOperations)
describe('server', () => {
const dbUri = process.env.MONGODB_URI ?? 'mongodb://localhost:27017'
@ -46,7 +46,7 @@ describe('server', () => {
params: [workspace, 'ООО Рога и Копыта']
}
const result = await methods.createWorkspace(db, request)
const result = await methods.createWorkspace(db, '', request)
expect(result.result).toBeDefined()
workspace = result.result as string
})
@ -57,12 +57,12 @@ describe('server', () => {
params: ['andrey2', '123']
}
const result = await methods.createAccount(db, request)
const result = await methods.createAccount(db, '', request)
expect(result.result).toBeDefined()
})
it('should not create, duplicate account', async () => {
await methods.createAccount(db, {
await methods.createAccount(db, '', {
method: 'createAccount',
params: ['andrey', '123']
})
@ -72,20 +72,20 @@ describe('server', () => {
params: ['andrey', '123']
}
const result = await methods.createAccount(db, request)
const result = await methods.createAccount(db, '', request)
expect(result.error).toBeDefined()
})
it('should login', async () => {
await methods.createAccount(db, {
await methods.createAccount(db, '', {
method: 'createAccount',
params: ['andrey', '123']
})
await methods.createWorkspace(db, {
await methods.createWorkspace(db, '', {
method: 'createWorkspace',
params: [workspace, 'ООО Рога и Копыта']
})
await methods.assignWorkspace(db, {
await methods.assignWorkspace(db, '', {
method: 'assignWorkspace',
params: ['andrey', workspace]
})
@ -95,7 +95,7 @@ describe('server', () => {
params: ['andrey', '123', workspace]
}
const result = await methods.login(db, request)
const result = await methods.login(db, '', request)
expect(result.result).toBeDefined()
})
@ -105,7 +105,7 @@ describe('server', () => {
params: ['andrey', '123555', workspace]
}
const result = await methods.login(db, request)
const result = await methods.login(db, '', request)
expect(result.error).toBeDefined()
})
@ -115,7 +115,7 @@ describe('server', () => {
params: ['andrey1', '123555', workspace]
}
const result = await methods.login(db, request)
const result = await methods.login(db, '', request)
expect(result.error).toBeDefined()
})
@ -125,34 +125,34 @@ describe('server', () => {
params: ['andrey', '123', 'non-existent-workspace']
}
const result = await methods.login(db, request)
const result = await methods.login(db, '', request)
expect(result.error).toBeDefined()
})
it('do remove workspace', async () => {
await methods.createAccount(db, {
await methods.createAccount(db, '', {
method: 'createAccount',
params: ['andrey', '123']
})
await methods.createWorkspace(db, {
await methods.createWorkspace(db, '', {
method: 'createWorkspace',
params: [workspace, 'ООО Рога и Копыта']
})
await methods.assignWorkspace(db, {
await methods.assignWorkspace(db, '', {
method: 'assignWorkspace',
params: ['andrey', workspace]
})
// Check we had one
expect((await getAccount(db, 'andrey'))?.workspaces.length).toEqual(1)
expect((await getWorkspace(db, workspace))?.accounts.length).toEqual(1)
expect((await getWorkspace(db, '', workspace))?.accounts.length).toEqual(1)
await methods.removeWorkspace(db, {
await methods.removeWorkspace(db, '', {
method: 'removeWorkspace',
params: ['andrey', workspace]
})
expect((await getAccount(db, 'andrey'))?.workspaces.length).toEqual(0)
expect((await getWorkspace(db, workspace))?.accounts.length).toEqual(0)
expect((await getWorkspace(db, '', workspace))?.accounts.length).toEqual(0)
})
afterAll(async () => {

View File

@ -21,7 +21,7 @@ import contact, {
Employee,
getAvatarColorForId
} from '@hcengineering/contact'
import core, { AccountRole, Data, Ref, Tx, TxOperations, Version } from '@hcengineering/core'
import core, { AccountRole, Data, getWorkspaceId, Ref, Tx, TxOperations, Version } from '@hcengineering/core'
import { MigrateOperation } from '@hcengineering/model'
import platform, {
getMetadata,
@ -37,7 +37,7 @@ import platform, {
import { decodeToken, generateToken } from '@hcengineering/server-token'
import toolPlugin, { connect, initModel, upgradeModel } from '@hcengineering/server-tool'
import { pbkdf2Sync, randomBytes } from 'crypto'
import { Binary, Db, ObjectId } from 'mongodb'
import { Binary, Db, Filter, ObjectId } from 'mongodb'
const WORKSPACE_COLLECTION = 'workspace'
const ACCOUNT_COLLECTION = 'account'
@ -121,6 +121,7 @@ export interface LoginInfo {
*/
export interface WorkspaceLoginInfo extends LoginInfo {
workspace: string
productId: string
}
/**
@ -154,16 +155,24 @@ export async function getAccount (db: Db, email: string): Promise<Account | null
return await db.collection(ACCOUNT_COLLECTION).findOne<Account>({ email })
}
function withProductId (productId: string, query: Filter<Workspace>): Filter<Workspace> {
return productId === ''
? {
$or: [
{ productId: '', ...query },
{ productId: { $exists: false }, ...query }
]
}
: { productId, ...query }
}
/**
* @public
* @param db -
* @param workspace -
* @returns
*/
export async function getWorkspace (db: Db, workspace: string): Promise<Workspace | null> {
return await db.collection(WORKSPACE_COLLECTION).findOne<Workspace>({
workspace
})
export async function getWorkspace (db: Db, productId: string, workspace: string): Promise<Workspace | null> {
return await db.collection(WORKSPACE_COLLECTION).findOne<Workspace>(withProductId(productId, { workspace }))
}
function toAccountInfo (account: Account): AccountInfo {
@ -191,12 +200,12 @@ async function getAccountInfo (db: Db, email: string, password: string): Promise
* @param workspace -
* @returns
*/
export async function login (db: Db, email: string, password: string): Promise<LoginInfo> {
export async function login (db: Db, productId: string, email: string, password: string): Promise<LoginInfo> {
await getAccountInfo(db, email, password)
const result = {
endpoint: getEndpoint(),
email,
token: generateToken(email, '')
token: generateToken(email, getWorkspaceId('', productId))
}
return result
}
@ -204,13 +213,18 @@ export async function login (db: Db, email: string, password: string): Promise<L
/**
* @public
*/
export async function selectWorkspace (db: Db, token: string, workspace: string): Promise<WorkspaceLoginInfo> {
export async function selectWorkspace (
db: Db,
productId: string,
token: string,
workspace: string
): Promise<WorkspaceLoginInfo> {
const { email } = decodeToken(token)
const accountInfo = await getAccount(db, email)
if (accountInfo === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: email }))
}
const workspaceInfo = await getWorkspace(db, workspace)
const workspaceInfo = await getWorkspace(db, productId, workspace)
if (workspaceInfo !== null) {
const workspaces = accountInfo.workspaces
@ -220,8 +234,9 @@ export async function selectWorkspace (db: Db, token: string, workspace: string)
const result = {
endpoint: getEndpoint(),
email,
token: generateToken(email, workspace),
workspace
token: generateToken(email, getWorkspaceId(workspace, productId)),
workspace,
productId
}
return result
}
@ -265,13 +280,19 @@ export async function useInvite (db: Db, inviteId: ObjectId): Promise<void> {
/**
* @public
*/
export async function join (db: Db, email: string, password: string, inviteId: ObjectId): Promise<WorkspaceLoginInfo> {
export async function join (
db: Db,
productId: string,
email: string,
password: string,
inviteId: ObjectId
): Promise<WorkspaceLoginInfo> {
const invite = await getInvite(db, inviteId)
const workspace = await checkInvite(invite, email)
await assignWorkspace(db, email, workspace)
await assignWorkspace(db, productId, email, workspace)
const token = (await login(db, email, password)).token
const result = await selectWorkspace(db, token, workspace)
const token = (await login(db, productId, email, password)).token
const result = await selectWorkspace(db, productId, token, workspace)
await useInvite(db, inviteId)
return result
}
@ -281,6 +302,7 @@ export async function join (db: Db, email: string, password: string, inviteId: O
*/
export async function signUpJoin (
db: Db,
productId: string,
email: string,
password: string,
first: string,
@ -289,11 +311,11 @@ export async function signUpJoin (
): Promise<WorkspaceLoginInfo> {
const invite = await getInvite(db, inviteId)
const workspace = await checkInvite(invite, email)
await createAccount(db, email, password, first, last)
await assignWorkspace(db, email, workspace)
await createAccount(db, productId, email, password, first, last)
await assignWorkspace(db, productId, email, workspace)
const token = (await login(db, email, password)).token
const result = await selectWorkspace(db, token, workspace)
const token = (await login(db, productId, email, password)).token
const result = await selectWorkspace(db, productId, token, workspace)
await useInvite(db, inviteId)
return result
}
@ -303,6 +325,7 @@ export async function signUpJoin (
*/
export async function createAccount (
db: Db,
productId: string,
email: string,
password: string,
first: string,
@ -328,7 +351,7 @@ export async function createAccount (
const result = {
endpoint: getEndpoint(),
email,
token: generateToken(email, '')
token: generateToken(email, getWorkspaceId('', productId))
}
return result
}
@ -337,13 +360,7 @@ export async function createAccount (
* @public
*/
export async function listWorkspaces (db: Db, productId: string): Promise<Workspace[]> {
if (productId === '') {
return await db
.collection<Workspace>(WORKSPACE_COLLECTION)
.find({ $or: [{ productId: { $exists: false } }, { productId: '' }] })
.toArray()
}
return await db.collection<Workspace>(WORKSPACE_COLLECTION).find({ productId }).toArray()
return await db.collection<Workspace>(WORKSPACE_COLLECTION).find(withProductId(productId, {})).toArray()
}
/**
@ -360,12 +377,12 @@ export async function createWorkspace (
version: Data<Version>,
txes: Tx[],
migrationOperation: MigrateOperation[],
productId: string,
db: Db,
productId: string,
workspace: string,
organisation: string
): Promise<string> {
if ((await getWorkspace(db, workspace)) !== null) {
if ((await getWorkspace(db, productId, workspace)) !== null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceAlreadyExists, { workspace }))
}
const result = await db
@ -377,7 +394,7 @@ export async function createWorkspace (
productId
})
.then((e) => e.insertedId.toHexString())
await initModel(getTransactor(), workspace, txes, migrationOperation)
await initModel(getTransactor(), getWorkspaceId(workspace, productId), txes, migrationOperation)
return result
}
@ -392,7 +409,7 @@ export async function upgradeWorkspace (
db: Db,
workspace: string
): Promise<string> {
const ws = await getWorkspace(db, workspace)
const ws = await getWorkspace(db, productId, workspace)
if (ws === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace }))
}
@ -407,7 +424,7 @@ export async function upgradeWorkspace (
$set: { version }
}
)
await upgradeModel(getTransactor(), workspace, txes, migrationOperation)
await upgradeModel(getTransactor(), getWorkspaceId(workspace, productId), txes, migrationOperation)
return `${version.major}.${version.minor}.${version.patch}`
}
@ -415,16 +432,17 @@ export async function upgradeWorkspace (
* @public
*/
export const createUserWorkspace =
(version: Data<Version>, txes: Tx[], migrationOperation: MigrateOperation[], productId: string) =>
async (db: Db, token: string, workspace: string): Promise<LoginInfo> => {
(version: Data<Version>, txes: Tx[], migrationOperation: MigrateOperation[]) =>
async (db: Db, productId: string, token: string, workspace: string): Promise<LoginInfo> => {
const { email } = decodeToken(token)
await createWorkspace(version, txes, migrationOperation, productId, db, workspace, '')
await assignWorkspace(db, email, workspace)
await setRole(email, workspace, AccountRole.Owner)
await createWorkspace(version, txes, migrationOperation, db, productId, workspace, '')
await assignWorkspace(db, productId, email, workspace)
await setRole(email, workspace, productId, AccountRole.Owner)
const result = {
endpoint: getEndpoint(),
email,
token: generateToken(email, workspace)
token: generateToken(email, getWorkspaceId(workspace, productId)),
productId
}
return result
}
@ -434,15 +452,18 @@ export const createUserWorkspace =
*/
export async function getInviteLink (
db: Db,
productId: string,
token: string,
exp: number,
emailMask: string,
limit: number
): Promise<ObjectId> {
const { workspace } = decodeToken(token)
const wsPromise = await getWorkspace(db, workspace)
const wsPromise = await getWorkspace(db, productId, workspace.name)
if (wsPromise === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace }))
throw new PlatformError(
new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace: workspace.name })
)
}
const result = await db.collection(INVITE_COLLECTION).insertOne({
workspace,
@ -456,24 +477,23 @@ export async function getInviteLink (
/**
* @public
*/
export async function getUserWorkspaces (db: Db, token: string): Promise<Workspace[]> {
export async function getUserWorkspaces (db: Db, productId: string, token: string): Promise<Workspace[]> {
const { email } = decodeToken(token)
const account = await getAccount(db, email)
if (account === null) return []
return await db
.collection<Workspace>(WORKSPACE_COLLECTION)
.find({
_id: { $in: account.workspaces }
})
.find(withProductId(productId, { _id: { $in: account.workspaces } }))
.toArray()
}
async function getWorkspaceAndAccount (
db: Db,
productId: string,
email: string,
workspace: string
): Promise<{ accountId: ObjectId, workspaceId: ObjectId }> {
const wsPromise = await getWorkspace(db, workspace)
const wsPromise = await getWorkspace(db, productId, workspace)
if (wsPromise === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace }))
}
@ -489,8 +509,8 @@ async function getWorkspaceAndAccount (
/**
* @public
*/
export async function setRole (email: string, workspace: string, role: AccountRole): Promise<void> {
const connection = await connect(getTransactor(), workspace, email)
export async function setRole (email: string, workspace: string, productId: string, role: AccountRole): Promise<void> {
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId), email)
try {
const ops = new TxOperations(connection, core.account.System)
@ -510,8 +530,8 @@ export async function setRole (email: string, workspace: string, role: AccountRo
/**
* @public
*/
export async function assignWorkspace (db: Db, email: string, workspace: string): Promise<void> {
const { workspaceId, accountId } = await getWorkspaceAndAccount(db, email, workspace)
export async function assignWorkspace (db: Db, productId: string, email: string, workspace: string): Promise<void> {
const { workspaceId, accountId } = await getWorkspaceAndAccount(db, productId, email, workspace)
// Add account into workspace.
await db.collection(WORKSPACE_COLLECTION).updateOne({ _id: workspaceId }, { $addToSet: { accounts: accountId } })
@ -520,7 +540,7 @@ export async function assignWorkspace (db: Db, email: string, workspace: string)
const account = await db.collection<Account>(ACCOUNT_COLLECTION).findOne({ _id: accountId })
if (account !== null) await createEmployeeAccount(account, workspace)
if (account !== null) await createEmployeeAccount(account, productId, workspace)
}
async function createEmployee (ops: TxOperations, name: string, email: string): Promise<Ref<Employee>> {
@ -542,8 +562,8 @@ async function createEmployee (ops: TxOperations, name: string, email: string):
return id
}
async function createEmployeeAccount (account: Account, workspace: string): Promise<void> {
const connection = await connect(getTransactor(), workspace, account.email)
async function createEmployeeAccount (account: Account, productId: string, workspace: string): Promise<void> {
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId), account.email)
try {
const ops = new TxOperations(connection, core.account.System)
@ -584,7 +604,13 @@ async function createEmployeeAccount (account: Account, workspace: string): Prom
/**
* @public
*/
export async function changePassword (db: Db, token: string, oldPassword: string, password: string): Promise<void> {
export async function changePassword (
db: Db,
productId: string,
token: string,
oldPassword: string,
password: string
): Promise<void> {
const { email } = decodeToken(token)
const account = await getAccountInfo(db, email, oldPassword)
@ -597,7 +623,7 @@ export async function changePassword (db: Db, token: string, oldPassword: string
/**
* @public
*/
export async function replacePassword (db: Db, email: string, password: string): Promise<void> {
export async function replacePassword (db: Db, productId: string, email: string, password: string): Promise<void> {
const account = await getAccount(db, email)
if (account === null) {
@ -612,7 +638,7 @@ export async function replacePassword (db: Db, email: string, password: string):
/**
* @public
*/
export async function changeName (db: Db, token: string, first: string, last: string): Promise<void> {
export async function changeName (db: Db, productId: string, token: string, first: string, last: string): Promise<void> {
const { email } = decodeToken(token)
const account = await getAccount(db, email)
if (account === null) {
@ -630,13 +656,15 @@ export async function changeName (db: Db, token: string, first: string, last: st
const promises: Promise<void>[] = []
for (const ws of workspaces) {
promises.push(updateEmployeeAccount(account, ws.workspace))
if (ws.productId === productId) {
promises.push(updateEmployeeAccount(account, ws.workspace, ws.productId))
}
}
await Promise.all(promises)
}
async function updateEmployeeAccount (account: Account, workspace: string): Promise<void> {
const connection = await connect(getTransactor(), workspace, account.email)
async function updateEmployeeAccount (account: Account, workspace: string, productId: string): Promise<void> {
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId), account.email)
try {
const ops = new TxOperations(connection, core.account.System)
@ -663,8 +691,8 @@ async function updateEmployeeAccount (account: Account, workspace: string): Prom
/**
* @public
*/
export async function removeWorkspace (db: Db, email: string, workspace: string): Promise<void> {
const { workspaceId, accountId } = await getWorkspaceAndAccount(db, email, workspace)
export async function removeWorkspace (db: Db, productId: string, email: string, workspace: string): Promise<void> {
const { workspaceId, accountId } = await getWorkspaceAndAccount(db, productId, email, workspace)
// Add account into workspace.
await db.collection(WORKSPACE_COLLECTION).updateOne({ _id: workspaceId }, { $pull: { accounts: accountId } })
@ -676,18 +704,23 @@ export async function removeWorkspace (db: Db, email: string, workspace: string)
/**
* @public
*/
export async function checkJoin (db: Db, token: string, inviteId: ObjectId): Promise<WorkspaceLoginInfo> {
export async function checkJoin (
db: Db,
productId: string,
token: string,
inviteId: ObjectId
): Promise<WorkspaceLoginInfo> {
const { email } = decodeToken(token)
const invite = await getInvite(db, inviteId)
const workspace = await checkInvite(invite, email)
return await selectWorkspace(db, token, workspace)
return await selectWorkspace(db, productId, token, workspace)
}
/**
* @public
*/
export async function dropWorkspace (db: Db, workspace: string): Promise<void> {
const ws = await getWorkspace(db, workspace)
export async function dropWorkspace (db: Db, productId: string, workspace: string): Promise<void> {
const ws = await getWorkspace(db, productId, workspace)
if (ws === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace }))
}
@ -700,7 +733,7 @@ export async function dropWorkspace (db: Db, workspace: string): Promise<void> {
/**
* @public
*/
export async function dropAccount (db: Db, email: string): Promise<void> {
export async function dropAccount (db: Db, productId: string, email: string): Promise<void> {
const account = await getAccount(db, email)
if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: email }))
@ -708,12 +741,12 @@ export async function dropAccount (db: Db, email: string): Promise<void> {
const workspaces = await db
.collection<Workspace>(WORKSPACE_COLLECTION)
.find({ _id: { $in: account.workspaces } })
.find(withProductId(productId, { _id: { $in: account.workspaces } }))
.toArray()
await Promise.all(
workspaces.map(async (ws) => {
return await deactivateEmployeeAccount(account.email, ws.workspace)
return await deactivateEmployeeAccount(account.email, ws.workspace, productId)
})
)
@ -726,7 +759,7 @@ export async function dropAccount (db: Db, email: string): Promise<void> {
/**
* @public
*/
export async function leaveWorkspace (db: Db, token: string, email: string): Promise<void> {
export async function leaveWorkspace (db: Db, productId: string, token: string, email: string): Promise<void> {
const tokenData = decodeToken(token)
const currentAccount = await getAccount(db, tokenData.email)
@ -736,14 +769,14 @@ export async function leaveWorkspace (db: Db, token: string, email: string): Pro
)
}
const workspace = await getWorkspace(db, tokenData.workspace)
const workspace = await getWorkspace(db, productId, tokenData.workspace.name)
if (workspace === null) {
throw new PlatformError(
new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace: tokenData.workspace })
new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace: tokenData.workspace.name })
)
}
await deactivateEmployeeAccount(email, workspace.workspace)
await deactivateEmployeeAccount(email, workspace.workspace, workspace.productId)
const account = tokenData.email !== email ? await getAccount(db, email) : currentAccount
if (account !== null) {
@ -756,8 +789,8 @@ export async function leaveWorkspace (db: Db, token: string, email: string): Pro
}
}
async function deactivateEmployeeAccount (email: string, workspace: string): Promise<void> {
const connection = await connect(getTransactor(), workspace, email)
async function deactivateEmployeeAccount (email: string, workspace: string, productId: string): Promise<void> {
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId), email)
try {
const ops = new TxOperations(connection, core.account.System)
@ -779,19 +812,27 @@ async function deactivateEmployeeAccount (email: string, workspace: string): Pro
/**
* @public
*/
export type AccountMethod = (db: Db, request: Request<any[]>, token?: string) => Promise<Response<any>>
export type AccountMethod = (
db: Db,
productId: string,
request: Request<any[]>,
token?: string
) => Promise<Response<any>>
function wrap (f: (db: Db, ...args: any[]) => Promise<any>): AccountMethod {
return async function (db: Db, request: Request<any[]>, token?: string): Promise<Response<any>> {
function wrap (f: (db: Db, productId: string, ...args: any[]) => Promise<any>): AccountMethod {
return async function (db: Db, productId: string, request: Request<any[]>, token?: string): Promise<Response<any>> {
if (token !== undefined) request.params.unshift(token)
return await f(db, ...request.params)
return await f(db, productId, ...request.params)
.then((result) => ({ id: request.id, result }))
.catch((err) => ({
error:
err instanceof PlatformError
? new Status(Severity.ERROR, platform.status.Forbidden, {})
: new Status(Severity.ERROR, platform.status.InternalServerError, {})
}))
.catch((err) => {
console.error(err)
return {
error:
err instanceof PlatformError
? new Status(Severity.ERROR, platform.status.Forbidden, {})
: new Status(Severity.ERROR, platform.status.InternalServerError, {})
}
})
}
}
@ -801,8 +842,7 @@ function wrap (f: (db: Db, ...args: any[]) => Promise<any>): AccountMethod {
export function getMethods (
version: Data<Version>,
txes: Tx[],
migrateOperations: MigrateOperation[],
productId: string
migrateOperations: MigrateOperation[]
): Record<string, AccountMethod> {
return {
login: wrap(login),
@ -814,7 +854,7 @@ export function getMethods (
getInviteLink: wrap(getInviteLink),
getAccountInfo: wrap(getAccountInfo),
createAccount: wrap(createAccount),
createWorkspace: wrap(createUserWorkspace(version, txes, migrateOperations, productId)),
createWorkspace: wrap(createUserWorkspace(version, txes, migrateOperations)),
assignWorkspace: wrap(assignWorkspace),
removeWorkspace: wrap(removeWorkspace),
leaveWorkspace: wrap(leaveWorkspace),

View File

@ -25,8 +25,7 @@
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5",
"@types/tar-stream": "^2.2.2",
"@types/node": "~16.11.12",
"@types/minio": "~7.0.11"
"@types/node": "~16.11.12"
},
"dependencies": {
"@hcengineering/platform": "^0.6.7",
@ -37,6 +36,6 @@
"@hcengineering/model": "~0.6.0",
"tar-stream": "^2.2.0",
"@hcengineering/server-tool": "~0.6.0",
"minio": "^7.0.26"
"@hcengineering/minio": "^0.6.0"
}
}

View File

@ -22,13 +22,14 @@ import core, {
Domain,
DOMAIN_MODEL,
DOMAIN_TRANSIENT,
Ref
Ref,
WorkspaceId
} from '@hcengineering/core'
import { connect } from '@hcengineering/server-tool'
import { createGzip } from 'node:zlib'
import { join } from 'path'
import { extract, Pack, pack } from 'tar-stream'
import { createGunzip, gunzipSync, gzipSync } from 'zlib'
import { connect } from '@hcengineering/server-tool'
import { BackupStorage } from './storage'
export * from './storage'
@ -72,6 +73,7 @@ export interface BackupSnapshot {
export interface BackupInfo {
workspace: string
version: string
productId: string
snapshots: BackupSnapshot[]
}
@ -107,8 +109,8 @@ async function loadDigest (
/**
* @public
*/
export async function backup (transactorUrl: string, dbName: string, storage: BackupStorage): Promise<void> {
const connection = (await connect(transactorUrl, dbName, undefined, {
export async function backup (transactorUrl: string, workspaceId: WorkspaceId, storage: BackupStorage): Promise<void> {
const connection = (await connect(transactorUrl, workspaceId, undefined, {
mode: 'backup'
})) as unknown as CoreClient & BackupClient
try {
@ -118,7 +120,8 @@ export async function backup (transactorUrl: string, dbName: string, storage: Ba
.filter((it) => it !== DOMAIN_TRANSIENT && it !== DOMAIN_MODEL)
let backupInfo: BackupInfo = {
workspace: dbName,
workspace: workspaceId.name,
productId: workspaceId.productId,
version: '0.6',
snapshots: []
}
@ -128,7 +131,8 @@ export async function backup (transactorUrl: string, dbName: string, storage: Ba
backupInfo = JSON.parse(gunzipSync(await storage.loadFile(infoFile)).toString())
}
backupInfo.workspace = dbName
backupInfo.workspace = workspaceId.name
backupInfo.productId = workspaceId.productId
const snapshot: BackupSnapshot = {
date: Date.now(),
@ -282,7 +286,7 @@ export async function backupList (storage: BackupStorage): Promise<void> {
*/
export async function restore (
transactorUrl: string,
dbName: string,
workspaceId: WorkspaceId,
storage: BackupStorage,
date: number
): Promise<void> {
@ -311,7 +315,7 @@ export async function restore (
Object.keys(s.domains).forEach((it) => domains.add(it as Domain))
}
const connection = (await connect(transactorUrl, dbName, undefined, {
const connection = (await connect(transactorUrl, workspaceId, undefined, {
mode: 'backup',
model: 'upgrade'
})) as unknown as CoreClient & BackupClient

View File

@ -1,6 +1,7 @@
import { WorkspaceId } from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import { createReadStream, createWriteStream, existsSync } from 'fs'
import { mkdir, readFile, writeFile } from 'fs/promises'
import { Client as MinioClient } from 'minio'
import { dirname, join } from 'path'
import { PassThrough, Readable, Writable } from 'stream'
@ -51,40 +52,25 @@ class FileStorage implements BackupStorage {
}
class MinioStorage implements BackupStorage {
constructor (readonly client: MinioClient, readonly bucketName: string, readonly root: string) {}
constructor (readonly client: MinioService, readonly workspaceId: WorkspaceId, readonly root: string) {}
async loadFile (name: string): Promise<Buffer> {
const data = await this.client.getObject(this.bucketName, join(this.root, name))
const chunks: Buffer[] = []
await new Promise((resolve) => {
data.on('readable', () => {
let chunk
while ((chunk = data.read()) !== null) {
const b = chunk as Buffer
chunks.push(b)
}
})
data.on('end', () => {
resolve(null)
})
})
return Buffer.concat(chunks)
const data = await this.client.read(this.workspaceId, join(this.root, name))
return Buffer.concat(data)
}
async write (name: string): Promise<Writable> {
const wr = new PassThrough()
void this.client.putObject(this.bucketName, join(this.root, name), wr)
void this.client.put(this.workspaceId, join(this.root, name), wr)
return wr
}
async load (name: string): Promise<Readable> {
return await this.client.getObject(this.bucketName, join(this.root, name))
return await this.client.get(this.workspaceId, join(this.root, name))
}
async exists (name: string): Promise<boolean> {
try {
await this.client.statObject(this.bucketName, join(this.root, name))
await this.client.stat(this.workspaceId, join(this.root, name))
return true
} catch (err) {
return false
@ -92,7 +78,7 @@ class MinioStorage implements BackupStorage {
}
async writeFile (name: string, data: string | Buffer): Promise<void> {
void this.client.putObject(this.bucketName, join(this.root, name), data, data.length)
void this.client.put(this.workspaceId, join(this.root, name), data, data.length)
}
}
@ -110,12 +96,12 @@ export async function createFileBackupStorage (fileName: string): Promise<Backup
* @public
*/
export async function createMinioBackupStorage (
client: MinioClient,
bucketName: string,
client: MinioService,
workspaceId: WorkspaceId,
root: string
): Promise<BackupStorage> {
if (!(await client.bucketExists(bucketName))) {
await client.makeBucket(bucketName, 'k8s')
if (!(await client.exists(workspaceId))) {
await client.make(workspaceId)
}
return new MinioStorage(client, bucketName, root)
return new MinioStorage(client, workspaceId, root)
}

View File

@ -15,7 +15,6 @@
"@hcengineering/platform-rig": "~0.6.0",
"@types/heft-jest": "^1.0.3",
"@types/node": "~16.11.12",
"@types/minio": "~7.0.11",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
@ -30,6 +29,6 @@
"dependencies": {
"@hcengineering/core": "^0.6.17",
"@hcengineering/platform": "^0.6.7",
"minio": "^7.0.26"
"@hcengineering/minio": "^0.6.0"
}
}

View File

@ -25,9 +25,10 @@ import {
Ref,
StorageIterator,
Tx,
TxResult
TxResult,
WorkspaceId
} from '@hcengineering/core'
import { Client } from 'minio'
import { MinioService } from '@hcengineering/minio'
/**
* @public
@ -65,9 +66,9 @@ export interface TxAdapter extends DbAdapter {
export type DbAdapterFactory = (
hierarchy: Hierarchy,
url: string,
db: string,
workspaceId: WorkspaceId,
modelDb: ModelDb,
storage?: Client
storage?: MinioService
) => Promise<DbAdapter>
/**
@ -129,6 +130,10 @@ class InMemoryAdapter implements DbAdapter {
/**
* @public
*/
export async function createInMemoryAdapter (hierarchy: Hierarchy, url: string, db: string): Promise<DbAdapter> {
export async function createInMemoryAdapter (
hierarchy: Hierarchy,
url: string,
workspaceId: WorkspaceId
): Promise<DbAdapter> {
return new InMemoryAdapter(hierarchy)
}

View File

@ -45,10 +45,11 @@ import core, {
TxProcessor,
TxRemoveDoc,
TxResult,
TxUpdateDoc
TxUpdateDoc,
WorkspaceId
} from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import { getResource } from '@hcengineering/platform'
import type { Client as MinioClient } from 'minio'
import { DbAdapter, DbAdapterConfiguration, TxAdapter } from './adapter'
import { FullTextIndex } from './fulltext'
import serverCore from './plugin'
@ -62,12 +63,12 @@ export interface DbConfiguration {
adapters: Record<string, DbAdapterConfiguration>
domains: Record<string, string>
defaultAdapter: string
workspace: string
workspace: WorkspaceId
fulltextAdapter: {
factory: FullTextAdapterFactory
url: string
}
storageFactory?: () => MinioClient
storageFactory?: () => MinioService
}
class TServerStorage implements ServerStorage {
@ -83,9 +84,9 @@ class TServerStorage implements ServerStorage {
hierarchy: Hierarchy,
private readonly triggers: Triggers,
private readonly fulltextAdapter: FullTextAdapter,
readonly storageAdapter: MinioClient | undefined,
readonly storageAdapter: MinioService | undefined,
readonly modelDb: ModelDb,
private readonly workspace: string,
private readonly workspace: WorkspaceId,
options?: ServerStorageOptions
) {
this.hierarchy = hierarchy

View File

@ -30,13 +30,12 @@ import type {
Storage,
Timestamp,
Tx,
TxResult
TxResult,
WorkspaceId
} from '@hcengineering/core'
import { Hierarchy, TxFactory } from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import type { Resource } from '@hcengineering/platform'
import type { Client as MinioClient, BucketItem } from 'minio'
export { MinioClient, BucketItem }
/**
* @public
@ -98,7 +97,7 @@ export interface Pipeline extends LowLevelStorage {
* @public
*/
export interface TriggerControl {
workspace: string
workspace: WorkspaceId
txFactory: TxFactory
findAll: Storage['findAll']
hierarchy: Hierarchy
@ -107,7 +106,7 @@ export interface TriggerControl {
fulltextFx: (f: (adapter: FullTextAdapter) => Promise<void>) => void
// Since we don't have other storages let's consider adapter is MinioClient
// Later can be replaced with generic one with bucket encapsulated inside.
storageFx: (f: (adapter: MinioClient, bucket: string) => Promise<void>) => void
storageFx: (f: (adapter: MinioService, workspaceId: WorkspaceId) => Promise<void>) => void
fx: (f: () => Promise<void>) => void
}
@ -157,7 +156,7 @@ export interface FullTextAdapter {
/**
* @public
*/
export type FullTextAdapterFactory = (url: string, workspace: string) => Promise<FullTextAdapter>
export type FullTextAdapterFactory = (url: string, workspace: WorkspaceId) => Promise<FullTextAdapter>
/**
* @public

View File

@ -14,13 +14,13 @@
// limitations under the License.
//
import type { Ref, Doc, Class, Account, Space } from '@hcengineering/core'
import { createElasticAdapter } from '../adapter'
import { Account, Class, Doc, getWorkspaceId, Ref, Space } from '@hcengineering/core'
import type { IndexedDoc } from '@hcengineering/server-core'
import { createElasticAdapter } from '../adapter'
describe('client', () => {
it('should create document', async () => {
const adapter = await createElasticAdapter('http://localhost:9200/', 'ws1')
const adapter = await createElasticAdapter('http://localhost:9200/', getWorkspaceId('ws1', ''))
const doc: IndexedDoc = {
id: 'doc1' as Ref<Doc>,
_class: 'class1' as Ref<Class<Doc>>,

View File

@ -14,12 +14,12 @@
// limitations under the License.
//
import type { Class, Doc, DocumentQuery, Ref, TxResult } from '@hcengineering/core'
import { Class, Doc, DocumentQuery, Ref, toWorkspaceString, TxResult, WorkspaceId } from '@hcengineering/core'
import type { FullTextAdapter, IndexedDoc } from '@hcengineering/server-core'
import { Client, errors as esErr } from '@elastic/elasticsearch'
class ElasticAdapter implements FullTextAdapter {
constructor (private readonly client: Client, private readonly db: string) {}
constructor (private readonly client: Client, private readonly workspaceId: WorkspaceId) {}
async close (): Promise<void> {
await this.client.close()
@ -95,7 +95,7 @@ class ElasticAdapter implements FullTextAdapter {
try {
const result = await this.client.search({
index: this.db,
index: toWorkspaceString(this.workspaceId),
body: {
query: request,
size: size ?? 200,
@ -113,14 +113,14 @@ class ElasticAdapter implements FullTextAdapter {
async index (doc: IndexedDoc): Promise<TxResult> {
if (doc.data === undefined) {
await this.client.index({
index: this.db,
index: toWorkspaceString(this.workspaceId),
id: doc.id,
type: '_doc',
body: doc
})
} else {
await this.client.index({
index: this.db,
index: toWorkspaceString(this.workspaceId),
id: doc.id,
type: '_doc',
pipeline: 'attachment',
@ -132,7 +132,7 @@ class ElasticAdapter implements FullTextAdapter {
async update (id: Ref<Doc>, update: Record<string, any>): Promise<TxResult> {
await this.client.update({
index: this.db,
index: toWorkspaceString(this.workspaceId),
id,
body: {
doc: update
@ -145,7 +145,7 @@ class ElasticAdapter implements FullTextAdapter {
async remove (id: Ref<Doc>): Promise<void> {
try {
await this.client.delete({
index: this.db,
index: toWorkspaceString(this.workspaceId),
id
})
} catch (e: any) {
@ -163,11 +163,11 @@ class ElasticAdapter implements FullTextAdapter {
*/
export async function createElasticAdapter (
url: string,
dbName: string
workspaceId: WorkspaceId
): Promise<FullTextAdapter & { close: () => Promise<void> }> {
const client = new Client({
node: url
})
return new ElasticAdapter(client, dbName)
return new ElasticAdapter(client, workspaceId)
}

View File

@ -13,6 +13,7 @@
// limitations under the License.
//
import { ApiResponse, Client } from '@elastic/elasticsearch'
import core, {
Class,
Doc,
@ -25,16 +26,17 @@ import core, {
Ref,
Space,
StorageIterator,
toWorkspaceString,
Tx,
TxResult
TxResult,
WorkspaceId
} from '@hcengineering/core'
import { PlatformError, unknownStatus } from '@hcengineering/platform'
import { DbAdapter, IndexedDoc } from '@hcengineering/server-core'
import { ApiResponse, Client } from '@elastic/elasticsearch'
import { createHash } from 'node:crypto'
class ElasticDataAdapter implements DbAdapter {
constructor (readonly db: string, readonly client: Client) {}
constructor (readonly workspaceId: WorkspaceId, readonly client: Client) {}
async findAll<T extends Doc>(
_class: Ref<Class<T>>,
@ -64,7 +66,7 @@ class ElasticDataAdapter implements DbAdapter {
next: async () => {
if (!listRecieved) {
const q = {
index: this.db,
index: toWorkspaceString(this.workspaceId),
type: '_doc',
scroll: '1s',
// search_type: 'scan', //if I use search_type then it requires size otherwise it shows 0 result
@ -125,7 +127,7 @@ class ElasticDataAdapter implements DbAdapter {
const result: Doc[] = []
const resp = await this.client.search({
index: this.db,
index: toWorkspaceString(this.workspaceId),
type: '_doc',
body: {
query: {
@ -159,7 +161,7 @@ class ElasticDataAdapter implements DbAdapter {
await this.client.deleteByQuery(
{
type: '_doc',
index: this.db,
index: toWorkspaceString(this.workspaceId),
body: {
query: {
terms: {
@ -174,7 +176,7 @@ class ElasticDataAdapter implements DbAdapter {
)
const operations = part.flatMap((doc) => [
{ index: { _index: this.db, _id: doc._id } },
{ index: { _index: this.workspaceId, _id: doc._id } },
(doc as FullTextData).data
])
@ -188,7 +190,7 @@ class ElasticDataAdapter implements DbAdapter {
await this.client.deleteByQuery(
{
type: '_doc',
index: this.db,
index: toWorkspaceString(this.workspaceId),
body: {
query: {
terms: {
@ -211,10 +213,10 @@ class ElasticDataAdapter implements DbAdapter {
export async function createElasticBackupDataAdapter (
hierarchy: Hierarchy,
url: string,
db: string
workspaceId: WorkspaceId
): Promise<DbAdapter> {
const client = new Client({
node: url
})
return new ElasticDataAdapter(db, client)
return new ElasticDataAdapter(workspaceId, client)
}

View File

@ -26,7 +26,6 @@
"@types/express-fileupload": "^1.1.7",
"@types/uuid": "^8.3.1",
"@types/cors": "^2.8.12",
"@types/minio": "~7.0.11",
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5",
@ -48,9 +47,9 @@
"@hcengineering/server-token": "~0.6.0",
"@hcengineering/attachment": "~0.6.1",
"@hcengineering/contrib": "~0.6.0",
"minio": "^7.0.26",
"body-parser": "~1.19.1",
"compression": "~1.7.4",
"sharp": "~0.30.7"
"sharp": "~0.30.7",
"@hcengineering/minio": "^0.6.0"
}
}

View File

@ -14,7 +14,7 @@
// limitations under the License.
//
import { Client } from 'minio'
import { MinioService } from '@hcengineering/minio'
import { setMetadata } from '@hcengineering/platform'
import serverToken from '@hcengineering/server-token'
import { start } from '.'
@ -51,7 +51,7 @@ if (minioSecretKey === undefined) {
process.exit(1)
}
const minio = new Client({
const minio = new MinioService({
endPoint: minioEndpoint,
port: 9000,
useSSL: false,

View File

@ -15,8 +15,9 @@
//
import attachment from '@hcengineering/attachment'
import { Account, Doc, Ref, Space } from '@hcengineering/core'
import { Account, Doc, Ref, Space, WorkspaceId } from '@hcengineering/core'
import { createElasticAdapter } from '@hcengineering/elastic'
import { MinioService } from '@hcengineering/minio'
import type { IndexedDoc } from '@hcengineering/server-core'
import { decodeToken, Token } from '@hcengineering/server-token'
import bp from 'body-parser'
@ -25,43 +26,22 @@ import cors from 'cors'
import express, { Response } from 'express'
import fileUpload, { UploadedFile } from 'express-fileupload'
import https from 'https'
import { BucketItem, Client, ItemBucketMetadata } from 'minio'
import { join, resolve } from 'path'
import { v4 as uuid } from 'uuid'
import sharp from 'sharp'
import { v4 as uuid } from 'uuid'
async function minioUpload (minio: Client, workspace: string, file: UploadedFile): Promise<string> {
async function minioUpload (minio: MinioService, workspace: WorkspaceId, file: UploadedFile): Promise<string> {
const id = uuid()
const meta: ItemBucketMetadata = {
const meta: any = {
'Content-Type': file.mimetype
}
const resp = await minio.putObject(workspace, id, file.data, file.size, meta)
const resp = await minio.put(workspace, id, file.data, file.size, meta)
console.log(resp)
return id
}
async function readMinioData (client: Client, db: string, name: string): Promise<Buffer[]> {
const data = await client.getObject(db, name)
const chunks: Buffer[] = []
await new Promise((resolve) => {
data.on('readable', () => {
let chunk
while ((chunk = data.read()) !== null) {
const b = chunk as Buffer
chunks.push(b)
}
})
data.on('end', () => {
resolve(null)
})
})
return chunks
}
function getRange (range: string, size: number): [number, number] {
const [startStr, endStr] = range.replace(/bytes=/, '').split('-')
@ -82,14 +62,14 @@ function getRange (range: string, size: number): [number, number] {
async function getFileRange (
range: string,
client: Client,
workspace: string,
client: MinioService,
workspace: WorkspaceId,
uuid: string,
res: Response
): Promise<void> {
const stat = await client.statObject(workspace, uuid)
const stat = await client.stat(workspace, uuid)
const size = stat.size
const size: number = stat.size
const [start, end] = getRange(range, size)
@ -101,13 +81,8 @@ async function getFileRange (
return
}
client.getPartialObject(workspace, uuid, start, end - start + 1, (err, dataStream) => {
if (err !== null) {
console.log(err)
res.status(500).send()
return
}
try {
const dataStream = await client.partial(workspace, uuid, start, end - start + 1)
res.writeHead(206, {
Connection: 'keep-alive',
'Content-Range': `bytes ${start}-${end}/${size}`,
@ -117,18 +92,17 @@ async function getFileRange (
})
dataStream.pipe(res)
})
} catch (err: any) {
console.log(err)
res.status(500).send()
}
}
async function getFile (client: Client, workspace: string, uuid: string, res: Response): Promise<void> {
const stat = await client.statObject(workspace, uuid)
async function getFile (client: MinioService, workspace: WorkspaceId, uuid: string, res: Response): Promise<void> {
const stat = await client.stat(workspace, uuid)
client.getObject(workspace, uuid, (err, dataStream) => {
if (err !== null) {
console.log(err)
res.status(500).send()
return
}
try {
const dataStream = await client.get(workspace, uuid)
res.status(200)
res.set('Cache-Control', 'max-age=604800')
@ -147,7 +121,10 @@ async function getFile (client: Client, workspace: string, uuid: string, res: Re
console.log(err)
res.status(500).send()
})
})
} catch (err: any) {
console.log(err)
res.status(500).send()
}
}
/**
@ -158,7 +135,7 @@ export function start (
config: {
transactorEndpoint: string
elasticUrl: string
minio: Client
minio: MinioService
accountsUrl: string
uploadUrl: string
modelVersion: string
@ -211,7 +188,7 @@ export function start (
const size = req.query.size as 'inline' | 'tiny' | 'x-small' | 'small' | 'medium' | 'large' | 'x-large' | 'full'
uuid = await getResizeID(size, uuid, config, payload)
const stat = await config.minio.statObject(payload.workspace, uuid)
const stat = await config.minio.stat(payload.workspace, uuid)
const fileSize = stat.size
@ -310,13 +287,14 @@ export function start (
const payload = decodeToken(token)
const uuid = req.query.file as string
await config.minio.removeObject(payload.workspace, uuid)
await config.minio.remove(payload.workspace, [uuid])
const extra = await listMinioObjects(config.minio, payload.workspace, uuid)
if (extra.size > 0) {
for (const e of extra.entries()) {
await config.minio.removeObject(payload.workspace, e[1].name)
}
const extra = await config.minio.list(payload.workspace, uuid)
if (extra.length > 0) {
await config.minio.remove(
payload.workspace,
Array.from(extra.entries()).map((it) => it[1].name)
)
}
res.status(200).send()
@ -365,7 +343,7 @@ export function start (
}
const id = uuid()
const contentType = response.headers['content-type']
const meta: ItemBucketMetadata = {
const meta = {
'Content-Type': contentType
}
const data: Buffer[] = []
@ -375,12 +353,9 @@ export function start (
})
.on('end', function () {
const buffer = Buffer.concat(data)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
config.minio.putObject(payload.workspace, id, buffer, 0, meta, async (err, objInfo) => {
if (err !== null) {
console.log('minio putObject error', err)
res.status(500).send(err)
} else {
config.minio
.put(payload.workspace, id, buffer, 0, meta)
.then(async (objInfo) => {
console.log('uploaded uuid', id)
if (attachedTo !== undefined) {
@ -405,8 +380,13 @@ export function start (
contentType,
size: buffer.length
})
}
})
})
.catch((err) => {
if (err !== null) {
console.log('minio putObject error', err)
res.status(500).send(err)
}
})
})
.on('error', function (err) {
res.status(500).send(err)
@ -454,7 +434,7 @@ export function start (
}
const id = uuid()
const contentType = response.headers['content-type']
const meta: ItemBucketMetadata = {
const meta = {
'Content-Type': contentType
}
const data: Buffer[] = []
@ -465,11 +445,9 @@ export function start (
.on('end', function () {
const buffer = Buffer.concat(data)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
config.minio.putObject(payload.workspace, id, buffer, 0, meta, async (err, objInfo) => {
if (err !== null) {
console.log('minio putObject error', err)
res.status(500).send(err)
} else {
config.minio
.put(payload.workspace, id, buffer, 0, meta)
.then(async () => {
console.log('uploaded uuid', id)
if (attachedTo !== undefined) {
@ -493,8 +471,11 @@ export function start (
contentType,
size: buffer.length
})
}
})
})
.catch((err) => {
console.log('minio putObject error', err)
res.status(500).send(err)
})
})
.on('error', function (err) {
res.status(500).send(err)
@ -515,7 +496,12 @@ export function start (
server.close()
}
}
async function getResizeID (size: string, uuid: string, config: { minio: Client }, payload: Token): Promise<string> {
async function getResizeID (
size: string,
uuid: string,
config: { minio: MinioService },
payload: Token
): Promise<string> {
if (size !== undefined && size !== 'full') {
let width = 64
switch (size) {
@ -536,7 +522,7 @@ async function getResizeID (size: string, uuid: string, config: { minio: Client
let hasSmall = false
const sizeId = uuid + `%size%${width}`
try {
const d = await config.minio.statObject(payload.workspace, sizeId)
const d = await config.minio.stat(payload.workspace, sizeId)
hasSmall = d !== undefined && d.size > 0
} catch (err) {}
if (hasSmall) {
@ -544,7 +530,7 @@ async function getResizeID (size: string, uuid: string, config: { minio: Client
uuid = sizeId
} else {
// Let's get data and resize it
const data = Buffer.concat(await readMinioData(config.minio, payload.workspace, uuid))
const data = Buffer.concat(await config.minio.read(payload.workspace, uuid))
const dataBuff = await sharp(data)
.resize({
@ -552,7 +538,7 @@ async function getResizeID (size: string, uuid: string, config: { minio: Client
})
.jpeg()
.toBuffer()
await config.minio.putObject(payload.workspace, sizeId, dataBuff, {
await config.minio.put(payload.workspace, sizeId, dataBuff, dataBuff.length, {
'Content-Type': 'image/jpeg'
})
uuid = sizeId
@ -560,21 +546,3 @@ async function getResizeID (size: string, uuid: string, config: { minio: Client
}
return uuid
}
async function listMinioObjects (
client: Client,
db: string,
prefix: string
): Promise<Map<string, BucketItem & { metaData: ItemBucketMetadata }>> {
const items = new Map<string, BucketItem & { metaData: ItemBucketMetadata }>()
const list = await client.listObjects(db, prefix, true)
await new Promise((resolve) => {
list.on('data', (data) => {
items.set(data.name, { metaData: {}, ...data })
})
list.on('end', () => {
resolve(null)
})
})
return items
}

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/config/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

4
server/minio/.npmignore Normal file
View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,18 @@
// The "rig.json" file directs tools to look for their config files in an external package.
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
/**
* (Required) The name of the rig package to inherit from.
* It should be an NPM package name with the "-rig" suffix.
*/
"rigPackageName": "@hcengineering/platform-rig"
/**
* (Optional) Selects a config profile from the rig package. The name must consist of
* lowercase alphanumeric words separated by hyphens, for example "sample-profile".
* If omitted, then the "default" profile will be used."
*/
// "rigProfile": "your-profile-name"
}

34
server/minio/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "@hcengineering/minio",
"version": "0.6.0",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "heft build",
"build:watch": "tsc",
"test": "heft test",
"lint:fix": "eslint --fix src",
"lint": "eslint src",
"format": "prettier --write src && eslint --fix src"
},
"devDependencies": {
"@hcengineering/platform-rig": "~0.6.0",
"@types/heft-jest": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.26.0",
"@typescript-eslint/parser": "^5.41.0",
"eslint-config-standard-with-typescript": "^23.0.0",
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5"
},
"dependencies": {
"@types/minio": "~7.0.11",
"@hcengineering/core": "^0.6.17",
"minio": "^7.0.26"
}
}

View File

@ -0,0 +1,24 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { getWorkspaceId } from '@hcengineering/core'
import { getBucketId } from '..'
describe('minio operations', () => {
it('check dot', async () => {
const wsid = getWorkspaceId('my-workspace', 'product')
expect(getBucketId(wsid)).toEqual('my-workspace.product')
})
})

119
server/minio/src/index.ts Normal file
View File

@ -0,0 +1,119 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { BucketItem, BucketItemStat, Client, ItemBucketMetadata, UploadedObjectInfo } from 'minio'
import { Readable as ReadableStream } from 'stream'
import { toWorkspaceString, WorkspaceId } from '@hcengineering/core'
/**
* @public
*/
export type MinioWorkspaceItem = BucketItem & { metaData: ItemBucketMetadata }
/**
* @public
*/
export function getBucketId (workspaceId: WorkspaceId): string {
return toWorkspaceString(workspaceId, '.')
}
/**
* @public
*/
export class MinioService {
client: Client
constructor (opt: { endPoint: string, port: number, accessKey: string, secretKey: string, useSSL: boolean }) {
this.client = new Client(opt)
}
async exists (workspaceId: WorkspaceId): Promise<boolean> {
return await this.client.bucketExists(getBucketId(workspaceId))
}
async make (workspaceId: WorkspaceId): Promise<void> {
await this.client.makeBucket(getBucketId(workspaceId), 'k8s')
}
async remove (workspaceId: WorkspaceId, objectNames: string[]): Promise<void> {
await this.client.removeObjects(getBucketId(workspaceId), objectNames)
}
async delete (workspaceId: WorkspaceId): Promise<void> {
await this.client.removeBucket(getBucketId(workspaceId))
}
async list (workspaceId: WorkspaceId, prefix?: string): Promise<MinioWorkspaceItem[]> {
const items = new Map<string, BucketItem & { metaData: ItemBucketMetadata }>()
const list = await this.client.listObjects(getBucketId(workspaceId), prefix, true)
await new Promise((resolve) => {
list.on('data', (data) => {
items.set(data.name, { metaData: {}, ...data })
})
list.on('end', () => {
resolve(null)
})
})
return Array.from(items.values())
}
async stat (workspaceId: WorkspaceId, objectName: string): Promise<BucketItemStat> {
return await this.client.statObject(getBucketId(workspaceId), objectName)
}
async get (workspaceId: WorkspaceId, objectName: string): Promise<ReadableStream> {
return await this.client.getObject(getBucketId(workspaceId), objectName)
}
async put (
workspaceId: WorkspaceId,
objectName: string,
stream: ReadableStream | Buffer | string,
size?: number,
metaData?: ItemBucketMetadata
): Promise<UploadedObjectInfo> {
return await this.client.putObject(getBucketId(workspaceId), objectName, stream, size, metaData)
}
async read (workspaceId: WorkspaceId, name: string): Promise<Buffer[]> {
const data = await this.client.getObject(getBucketId(workspaceId), name)
const chunks: Buffer[] = []
await new Promise((resolve) => {
data.on('readable', () => {
let chunk
while ((chunk = data.read()) !== null) {
const b = chunk as Buffer
chunks.push(b)
}
})
data.on('end', () => {
resolve(null)
})
})
return chunks
}
async partial (
workspaceId: WorkspaceId,
objectName: string,
offset: number,
length?: number
): Promise<ReadableStream> {
return await this.client.getPartialObject(getBucketId(workspaceId), objectName, offset, length)
}
}

View File

@ -0,0 +1,8 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib"
}
}

View File

@ -30,6 +30,6 @@
"@hcengineering/core": "^0.6.17",
"@hcengineering/platform": "^0.6.7",
"@hcengineering/server-core": "~0.6.1",
"mongodb": "^4.9.0"
"mongodb": "^4.11.0"
}
}

View File

@ -27,6 +27,7 @@ import core, {
FindOptions,
FindResult,
generateId,
getWorkspaceId,
Hierarchy,
MeasureMetricsContext,
ModelDb,
@ -37,7 +38,8 @@ import core, {
toFindResult,
Tx,
TxOperations,
TxResult
TxResult,
WorkspaceId
} from '@hcengineering/core'
import {
createServerStorage,
@ -88,7 +90,12 @@ class NullDbAdapter implements DbAdapter {
async clean (domain: Domain, docs: Ref<Doc>[]): Promise<void> {}
}
async function createNullAdapter (hierarchy: Hierarchy, url: string, db: string, modelDb: ModelDb): Promise<DbAdapter> {
async function createNullAdapter (
hierarchy: Hierarchy,
url: string,
db: WorkspaceId,
modelDb: ModelDb
): Promise<DbAdapter> {
return new NullDbAdapter()
}
@ -153,7 +160,7 @@ describe('mongo operations', () => {
await model.tx(t)
}
const txStorage = await createMongoTxAdapter(hierarchy, mongodbUri, dbId, model)
const txStorage = await createMongoTxAdapter(hierarchy, mongodbUri, getWorkspaceId(dbId, ''), model)
// Put all transactions to Tx
for (const t of txes) {
@ -184,7 +191,7 @@ describe('mongo operations', () => {
factory: createNullFullTextAdapter,
url: ''
},
workspace: dbId
workspace: getWorkspaceId(dbId, '')
}
const serverStorage = await createServerStorage(conf)
const ctx = new MeasureMetricsContext('client', {})

View File

@ -15,3 +15,4 @@
//
export * from './storage'
export * from './utils'

View File

@ -42,12 +42,13 @@ import core, {
TxRemoveDoc,
TxResult,
TxUpdateDoc,
WithLookup
WithLookup,
WorkspaceId
} from '@hcengineering/core'
import type { DbAdapter, TxAdapter } from '@hcengineering/server-core'
import { Collection, Db, Document, Filter, MongoClient, Sort, UpdateFilter } from 'mongodb'
import { createHash } from 'node:crypto'
import { getMongoClient } from './utils'
import { getMongoClient, getWorkspaceDB } from './utils'
function translateDoc (doc: Doc): Document {
return doc as Document
@ -779,11 +780,11 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
export async function createMongoAdapter (
hierarchy: Hierarchy,
url: string,
dbName: string,
workspaceId: WorkspaceId,
modelDb: ModelDb
): Promise<DbAdapter> {
const client = await getMongoClient(url)
const db = client.db(dbName)
const db = getWorkspaceDB(client, workspaceId)
return new MongoAdapter(db, hierarchy, modelDb, client)
}
@ -793,10 +794,10 @@ export async function createMongoAdapter (
export async function createMongoTxAdapter (
hierarchy: Hierarchy,
url: string,
dbName: string,
workspaceId: WorkspaceId,
modelDb: ModelDb
): Promise<TxAdapter> {
const client = await getMongoClient(url)
const db = client.db(dbName)
const db = getWorkspaceDB(client, workspaceId)
return new MongoTxAdapter(db, hierarchy, modelDb, client)
}

View File

@ -13,7 +13,8 @@
// limitations under the License.
//
import { MongoClient, MongoClientOptions } from 'mongodb'
import { toWorkspaceString, WorkspaceId } from '@hcengineering/core'
import { Db, MongoClient, MongoClientOptions } from 'mongodb'
let connections: MongoClient[] = []
@ -40,3 +41,12 @@ export async function getMongoClient (uri: string, options?: MongoClientOptions)
connections.push(client)
return client
}
/**
* @public
*
* Construct MongoDB table from workspace.
*/
export function getWorkspaceDB (client: MongoClient, workspaceId: WorkspaceId): Db {
return client.db(toWorkspaceString(workspaceId))
}

View File

@ -27,8 +27,7 @@
"eslint-config-standard-with-typescript": "^23.0.0",
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5",
"@types/minio": "~7.0.11"
"typescript": "^4.3.5"
},
"dependencies": {
"@hcengineering/core": "^0.6.17",
@ -38,8 +37,8 @@
"@hcengineering/mongo": "~0.6.1",
"@hcengineering/elastic": "~0.6.0",
"elastic-apm-node": "~3.26.0",
"minio": "^7.0.26",
"@hcengineering/server-token": "~0.6.0",
"@hcengineering/middleware": "~0.6.0"
"@hcengineering/middleware": "~0.6.0",
"@hcengineering/minio": "^0.6.0"
}
}

View File

@ -14,6 +14,7 @@
//
import core, {
BlobData,
Class,
Doc,
DocumentQuery,
@ -23,17 +24,17 @@ import core, {
Hierarchy,
ModelDb,
Ref,
Space,
StorageIterator,
Tx,
TxResult,
BlobData,
Space
WorkspaceId
} from '@hcengineering/core'
import { MinioService, MinioWorkspaceItem } from '@hcengineering/minio'
import { DbAdapter } from '@hcengineering/server-core'
import { BucketItem, Client, ItemBucketMetadata } from 'minio'
class MinioBlobAdapter implements DbAdapter {
constructor (readonly db: string, readonly client: Client) {}
constructor (readonly workspaceId: WorkspaceId, readonly client: MinioService) {}
async findAll<T extends Doc>(
_class: Ref<Class<T>>,
@ -51,48 +52,14 @@ class MinioBlobAdapter implements DbAdapter {
async close (): Promise<void> {}
async listMinioObjects (): Promise<Map<string, BucketItem & { metaData: ItemBucketMetadata }>> {
const items = new Map<string, BucketItem & { metaData: ItemBucketMetadata }>()
const list = await this.client.listObjects(this.db, undefined, true)
await new Promise((resolve) => {
list.on('data', (data) => {
items.set(data.name, { metaData: {}, ...data })
})
list.on('end', () => {
resolve(null)
})
})
return items
}
async readMinioData (name: string): Promise<Buffer[]> {
const data = await this.client.getObject(this.db, name)
const chunks: Buffer[] = []
await new Promise((resolve) => {
data.on('readable', () => {
let chunk
while ((chunk = data.read()) !== null) {
const b = chunk as Buffer
chunks.push(b)
}
})
data.on('end', () => {
resolve(null)
})
})
return chunks
}
find (domain: Domain): StorageIterator {
let listRecieved = false
let items: (BucketItem & { metaData: ItemBucketMetadata })[]
let items: MinioWorkspaceItem[] = []
let pos = 0
return {
next: async () => {
if (!listRecieved) {
items = Array.from((await this.listMinioObjects()).values())
items = await this.client.list(this.workspaceId)
listRecieved = true
}
if (pos < items?.length) {
@ -113,8 +80,8 @@ class MinioBlobAdapter implements DbAdapter {
async load (domain: Domain, docs: Ref<Doc>[]): Promise<Doc[]> {
const result: Doc[] = []
for (const item of docs) {
const stat = await this.client.statObject(this.db, item)
const chunks: Buffer[] = await this.readMinioData(item)
const stat = await this.client.stat(this.workspaceId, item)
const chunks: Buffer[] = await this.client.read(this.workspaceId, item)
const final = Buffer.concat(chunks)
const dta: BlobData = {
_id: item as Ref<BlobData>,
@ -142,12 +109,12 @@ class MinioBlobAdapter implements DbAdapter {
const blob = d as unknown as BlobData
// Remove existing document
try {
await this.client.removeObject(this.db, blob._id)
await this.client.remove(this.workspaceId, [blob._id])
} catch (ee) {
// ignore error
}
const buffer = Buffer.from(blob.base64Data, 'base64')
await this.client.putObject(this.db, blob._id, buffer, buffer.length, {
await this.client.put(this.workspaceId, blob._id, buffer, buffer.length, {
'Content-Type': blob.type,
lastModified: new Date(blob.modifiedOn)
})
@ -155,7 +122,7 @@ class MinioBlobAdapter implements DbAdapter {
}
async clean (domain: Domain, docs: Ref<Doc>[]): Promise<void> {
await this.client.removeObjects(this.db, docs)
await this.client.remove(this.workspaceId, docs)
}
}
@ -165,12 +132,12 @@ class MinioBlobAdapter implements DbAdapter {
export async function createMinioDataAdapter (
hierarchy: Hierarchy,
url: string,
db: string,
workspaceId: WorkspaceId,
modelDb: ModelDb,
storage?: Client
storage?: MinioService
): Promise<DbAdapter> {
if (storage === undefined) {
throw new Error('minio storage adapter require minio')
}
return new MinioBlobAdapter(db, storage)
return new MinioBlobAdapter(workspaceId, storage)
}

View File

@ -26,7 +26,8 @@ import {
StorageIterator,
toFindResult,
Tx,
TxResult
TxResult,
WorkspaceId
} from '@hcengineering/core'
import { DbAdapter } from '@hcengineering/server-core'
@ -68,7 +69,7 @@ class NullDbAdapter implements DbAdapter {
export async function createNullAdapter (
hierarchy: Hierarchy,
url: string,
db: string,
workspaceId: WorkspaceId,
modelDb: ModelDb
): Promise<DbAdapter> {
return new NullDbAdapter()

View File

@ -15,7 +15,6 @@
"@hcengineering/platform-rig": "~0.6.0",
"@types/heft-jest": "^1.0.3",
"@types/node": "~16.11.12",
"@types/minio": "~7.0.11",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
@ -30,7 +29,6 @@
"dependencies": {
"@hcengineering/core": "^0.6.17",
"@hcengineering/platform": "^0.6.7",
"minio": "^7.0.26",
"jwt-simple": "~0.5.6"
}
}

View File

@ -1,13 +1,14 @@
import { getWorkspaceId, WorkspaceId } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import { decode, encode } from 'jwt-simple'
import serverPlugin from './plugin'
import { encode, decode } from 'jwt-simple'
/**
* @public
*/
export interface Token {
email: string
workspace: string
workspace: WorkspaceId
extra?: Record<string, string>
}
@ -18,8 +19,8 @@ const getSecret = (): string => {
/**
* @public
*/
export function generateToken (email: string, workspace: string, extra?: Record<string, string>): string {
return encode({ ...(extra ?? {}), email, workspace }, getSecret())
export function generateToken (email: string, workspace: WorkspaceId, extra?: Record<string, string>): string {
return encode({ ...(extra ?? {}), email, workspace: workspace.name, productId: workspace.productId }, getSecret())
}
/**
@ -27,6 +28,6 @@ export function generateToken (email: string, workspace: string, extra?: Record<
*/
export function decodeToken (token: string): Token {
const value = decode(token, getSecret(), false)
const { email, workspace, ...extra } = value
return { email, workspace, extra }
const { email, workspace, productId, ...extra } = value
return { email, workspace: getWorkspaceId(workspace, productId), extra }
}

View File

@ -24,19 +24,19 @@
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5",
"@types/minio": "~7.0.11",
"@types/ws": "^8.5.3"
},
"dependencies": {
"mongodb": "^4.9.0",
"mongodb": "^4.11.0",
"@hcengineering/platform": "^0.6.7",
"minio": "^7.0.26",
"@hcengineering/core": "^0.6.17",
"@hcengineering/contact": "~0.6.8",
"@hcengineering/client-resources": "~0.6.4",
"@hcengineering/client": "^0.6.3",
"ws": "^8.10.0",
"@hcengineering/model": "~0.6.0",
"@hcengineering/server-token": "~0.6.0"
"@hcengineering/server-token": "~0.6.0",
"@hcengineering/mongo": "~0.6.1",
"@hcengineering/minio": "^0.6.0"
}
}

View File

@ -16,7 +16,7 @@
import client from '@hcengineering/client'
import clientResources from '@hcengineering/client-resources'
import { Client } from '@hcengineering/core'
import { Client, WorkspaceId } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform'
import { generateToken } from '@hcengineering/server-token'
@ -25,7 +25,7 @@ import { generateToken } from '@hcengineering/server-token'
*/
export async function connect (
transactorUrl: string,
workspace: string,
workspace: WorkspaceId,
email?: string,
extra?: Record<string, string>
): Promise<Client> {

View File

@ -14,9 +14,18 @@
//
import contact from '@hcengineering/contact'
import core, { Client as CoreClient, Domain, DOMAIN_MODEL, DOMAIN_TX, IndexKind, Tx } from '@hcengineering/core'
import core, {
Client as CoreClient,
Domain,
DOMAIN_MODEL,
DOMAIN_TX,
IndexKind,
Tx,
WorkspaceId
} from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import { MigrateOperation } from '@hcengineering/model'
import { Client } from 'minio'
import { getWorkspaceDB } from '@hcengineering/mongo'
import { Db, Document, MongoClient } from 'mongodb'
import { connect } from './connect'
import toolPlugin from './plugin'
@ -29,7 +38,7 @@ export { toolPlugin as default }
/**
* @public
*/
export function prepareTools (rawTxes: Tx[]): { mongodbUri: string, minio: Client, txes: Tx[] } {
export function prepareTools (rawTxes: Tx[]): { mongodbUri: string, minio: MinioService, txes: Tx[] } {
let minioEndpoint = process.env.MINIO_ENDPOINT
if (minioEndpoint === undefined) {
console.error('please provide minio endpoint')
@ -61,7 +70,7 @@ export function prepareTools (rawTxes: Tx[]): { mongodbUri: string, minio: Clien
minioPort = parseInt(sp[1])
}
const minio = new Client({
const minio = new MinioService({
endPoint: minioEndpoint,
port: minioPort,
useSSL: false,
@ -77,7 +86,7 @@ export function prepareTools (rawTxes: Tx[]): { mongodbUri: string, minio: Clien
*/
export async function initModel (
transactorUrl: string,
dbName: string,
workspaceId: WorkspaceId,
rawTxes: Tx[],
migrateOperations: MigrateOperation[]
): Promise<void> {
@ -89,7 +98,7 @@ export async function initModel (
const client = new MongoClient(mongodbUri)
try {
await client.connect()
const db = client.db(dbName)
const db = getWorkspaceDB(client, workspaceId)
console.log('dropping database...')
await db.dropDatabase()
@ -100,7 +109,7 @@ export async function initModel (
console.log(`${result.insertedCount} model transactions inserted.`)
console.log('creating data...')
const connection = await connect(transactorUrl, dbName, undefined, { model: 'upgrade' })
const connection = await connect(transactorUrl, workspaceId, undefined, { model: 'upgrade' })
try {
for (const op of migrateOperations) {
await op.upgrade(connection)
@ -115,8 +124,8 @@ export async function initModel (
await createUpdateIndexes(connection, db)
console.log('create minio bucket')
if (!(await minio.bucketExists(dbName))) {
await minio.makeBucket(dbName, 'k8s')
if (!(await minio.exists(workspaceId))) {
await minio.make(workspaceId)
}
} finally {
await client.close()
@ -128,7 +137,7 @@ export async function initModel (
*/
export async function upgradeModel (
transactorUrl: string,
dbName: string,
workspaceId: WorkspaceId,
rawTxes: Tx[],
migrateOperations: MigrateOperation[]
): Promise<void> {
@ -141,7 +150,7 @@ export async function upgradeModel (
const client = new MongoClient(mongodbUri)
try {
await client.connect()
const db = client.db(dbName)
const db = getWorkspaceDB(client, workspaceId)
console.log('removing model...')
// we're preserving accounts (created by core.account.System).
@ -164,7 +173,7 @@ export async function upgradeModel (
console.log('Apply upgrade operations')
const connection = await connect(transactorUrl, dbName, undefined, { model: 'upgrade' })
const connection = await connect(transactorUrl, workspaceId, undefined, { model: 'upgrade' })
// Create update indexes
await createUpdateIndexes(connection, db)

View File

@ -26,6 +26,7 @@ import {
Domain,
FindOptions,
FindResult,
getWorkspaceId,
Hierarchy,
MeasureMetricsContext,
ModelDb,
@ -78,11 +79,12 @@ describe('server', () => {
clean: async (domain: Domain, docs: Ref<Doc>[]) => {}
}),
(token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
3335
3335,
''
)
function connect (): WebSocket {
const token: string = generateToken('', 'latest')
const token: string = generateToken('', getWorkspaceId('latest', ''))
return new WebSocket(`ws://localhost:3335/${token}`)
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import core, { MeasureContext, Ref, Space, TxFactory } from '@hcengineering/core'
import core, { MeasureContext, Ref, Space, toWorkspaceString, TxFactory, WorkspaceId } from '@hcengineering/core'
import { readRequest, Response, serialize, UNAUTHORIZED, unknownError } from '@hcengineering/platform'
import type { Pipeline } from '@hcengineering/server-core'
import { decodeToken, Token } from '@hcengineering/server-token'
@ -46,9 +46,11 @@ class SessionManager {
ctx: MeasureContext,
ws: WebSocket,
token: Token,
pipelineFactory: (ws: string) => Promise<Pipeline>
pipelineFactory: (ws: WorkspaceId) => Promise<Pipeline>,
productId: string
): Promise<Session> {
const workspace = this.workspaces.get(token.workspace)
const wsString = toWorkspaceString(token.workspace, '@')
const workspace = this.workspaces.get(wsString)
if (workspace === undefined) {
return await this.createWorkspace(ctx, pipelineFactory, token, ws)
} else {
@ -115,7 +117,7 @@ class SessionManager {
private async createWorkspace (
ctx: MeasureContext,
pipelineFactory: (ws: string) => Promise<Pipeline>,
pipelineFactory: (ws: WorkspaceId) => Promise<Pipeline>,
token: Token,
ws: WebSocket
): Promise<Session> {
@ -126,14 +128,21 @@ class SessionManager {
sessions: [[session, ws]],
upgrade: token.extra?.model === 'upgrade'
}
this.workspaces.set(token.workspace, workspace)
this.workspaces.set(toWorkspaceString(token.workspace), workspace)
await this.setStatus(ctx, session, true)
return session
}
async close (ctx: MeasureContext, ws: WebSocket, workspaceId: string, code: number, reason: string): Promise<void> {
async close (
ctx: MeasureContext,
ws: WebSocket,
workspaceId: WorkspaceId,
code: number,
reason: string
): Promise<void> {
if (LOGGING_ENABLED) console.log(`closing websocket, code: ${code}, reason: ${reason}`)
const workspace = this.workspaces.get(workspaceId)
const wsid = toWorkspaceString(workspaceId)
const workspace = this.workspaces.get(wsid)
if (workspace === undefined) {
console.error(new Error('internal: cannot find sessions'))
return
@ -149,15 +158,15 @@ class SessionManager {
await this.setStatus(ctx, session[0], false)
}
if (workspace.sessions.length === 0) {
if (LOGGING_ENABLED) console.log('no sessions for workspace', workspaceId)
this.workspaces.delete(workspaceId)
if (LOGGING_ENABLED) console.log('no sessions for workspace', wsid)
this.workspaces.delete(wsid)
await workspace.pipeline.close().catch((err) => console.error(err))
}
}
}
broadcast (from: Session | null, workspaceId: string, resp: Response<any>, target?: string): void {
const workspace = this.workspaces.get(workspaceId)
broadcast (from: Session | null, workspaceId: WorkspaceId, resp: Response<any>, target?: string): void {
const workspace = this.workspaces.get(toWorkspaceString(workspaceId))
if (workspace === undefined) {
console.error(new Error('internal: cannot find sessions'))
return
@ -210,9 +219,10 @@ async function handleRequest<S extends Session> (
*/
export function start (
ctx: MeasureContext,
pipelineFactory: (workspace: string) => Promise<Pipeline>,
pipelineFactory: (workspace: WorkspaceId) => Promise<Pipeline>,
sessionFactory: (token: Token, pipeline: Pipeline, broadcast: BroadcastCall) => Session,
port: number,
productId: string,
host?: string
): () => void {
console.log(`starting server on port ${port} ...`)
@ -244,12 +254,13 @@ export function start (
ws.on('message', (msg: string) => {
buffer.push(msg)
})
const session = await sessions.addSession(ctx, ws, token, pipelineFactory)
const session = await sessions.addSession(ctx, ws, token, pipelineFactory, productId)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
ws.on('message', async (msg: string) => await handleRequest(ctx, session, ws, msg))
// eslint-disable-next-line @typescript-eslint/no-misused-promises
ws.on('close', async (code: number, reason: string) => await sessions.close(ctx, ws, token.workspace, code, reason))
ws.on('close', (code: number, reason: string) => {
void sessions.close(ctx, ws, token.workspace, code, reason)
})
for (const msg of buffer) {
await handleRequest(ctx, session, ws, msg)
}
@ -261,6 +272,11 @@ export function start (
try {
const payload = decodeToken(token ?? '')
console.log('client connected with payload', payload)
if (productId !== '' && payload.workspace.productId !== productId) {
throw new Error('Invalid workspace product')
}
wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request, payload))
} catch (err) {
wss.handleUpgrade(request, socket, head, (ws) => {

View File

@ -7,7 +7,8 @@ import {
MeasureContext,
Ref,
Tx,
TxResult
TxResult,
WorkspaceId
} from '@hcengineering/core'
import { Response } from '@hcengineering/platform'
import { Pipeline } from '@hcengineering/server-core'
@ -31,4 +32,9 @@ export interface Session {
/**
* @public
*/
export type BroadcastCall = (from: Session | null, workspaceId: string, resp: Response<any>, target?: string) => void
export type BroadcastCall = (
from: Session | null,
workspaceId: WorkspaceId,
resp: Response<any>,
target?: string
) => void