From 8e5be85a81812f6ddfc7fadd2a92ff698844f926 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Thu, 9 Oct 2025 23:22:16 +0700 Subject: [PATCH] Remove moved to huly.server packages Signed-off-by: Andrey Sobolev --- common/config/rush/pnpm-lock.yaml | 1465 ++++++------ common/config/rush/version-policies.json | 70 +- common/scripts/package.json | 3 +- common/scripts/safe-publish.js | 158 ++ plugins/guest/package.json | 4 + pods/fulltext/package.json | 2 +- pods/fulltext/src/__tests__/indexing.spec.ts | 2 +- pods/fulltext/src/workspace.ts | 4 +- pods/server/package.json | 2 +- rush.json | 101 +- server/client/.eslintrc.js | 7 - server/client/.npmignore | 4 - server/client/config/rig.json | 5 - server/client/jest.config.js | 7 - server/client/package.json | 48 - server/client/src/account.ts | 122 - server/client/src/blob.ts | 108 - server/client/src/client.ts | 72 - server/client/src/index.ts | 24 - server/client/src/plugin.ts | 18 - server/client/src/token.ts | 52 - server/client/tsconfig.json | 12 - server/collaboration/.eslintrc.js | 7 - server/collaboration/.npmignore | 4 - server/collaboration/config/rig.json | 5 - server/collaboration/jest.config.js | 7 - server/collaboration/package.json | 44 - server/collaboration/src/index.ts | 17 - server/collaboration/src/storage.ts | 126 - server/collaboration/src/ydoc.ts | 82 - server/collaboration/tsconfig.json | 12 - server/core/.eslintrc.js | 7 - server/core/.npmignore | 4 - server/core/CHANGELOG.json | 17 - server/core/CHANGELOG.md | 9 - server/core/config/rig.json | 5 - server/core/jest.config.js | 7 - server/core/package.json | 50 - server/core/src/adapter.ts | 124 - server/core/src/base.ts | 173 -- server/core/src/benchmark/index.ts | 134 -- server/core/src/configuration.ts | 47 - server/core/src/content.ts | 52 - server/core/src/dbAdapterManager.ts | 228 -- server/core/src/domainHelper.ts | 192 -- server/core/src/index.ts | 37 - server/core/src/limitter.ts | 1 - server/core/src/mem.ts | 189 -- server/core/src/nullAdapter.ts | 31 - server/core/src/pipeline.ts | 155 -- server/core/src/plugin.ts | 46 - server/core/src/queue/dummyQueue.ts | 76 - server/core/src/queue/index.ts | 5 - server/core/src/queue/types.ts | 78 - server/core/src/queue/users.ts | 32 - server/core/src/queue/utils.ts | 5 - server/core/src/queue/workspace.ts | 48 - server/core/src/service.ts | 54 - server/core/src/stats.ts | 165 -- server/core/src/storage.ts | 117 - server/core/src/triggers.ts | 197 -- server/core/src/types.ts | 780 ------ server/core/src/utils.ts | 412 ---- server/core/tsconfig.json | 12 - server/datalake/.eslintrc.js | 7 - server/datalake/.npmignore | 4 - server/datalake/config/rig.json | 5 - server/datalake/jest.config.js | 7 - server/datalake/package.json | 43 - server/datalake/src/__tests__/utils.test.ts | 40 - server/datalake/src/client.ts | 545 ----- server/datalake/src/error.ts | 35 - server/datalake/src/index.ts | 289 --- server/datalake/src/perfTest.ts | 106 - server/datalake/src/utils.ts | 32 - server/datalake/tsconfig.json | 12 - server/elastic/.eslintrc.js | 7 - server/elastic/.npmignore | 4 - server/elastic/config/rig.json | 4 - server/elastic/jest.config.js | 7 - server/elastic/package.json | 52 - server/elastic/src/__tests__/adapter.test.ts | 63 - server/elastic/src/adapter.ts | 707 ------ server/elastic/src/index.ts | 17 - server/elastic/tsconfig.json | 12 - server/kafka/.eslintrc.js | 7 - server/kafka/.npmignore | 4 - server/kafka/config/rig.json | 5 - server/kafka/jest.config.js | 7 - server/kafka/package.json | 43 - server/kafka/src/__test__/queue.spec.ts | 66 - server/kafka/src/index.ts | 333 --- server/kafka/tsconfig.json | 12 - server/middleware/.eslintrc.js | 7 - server/middleware/.npmignore | 4 - server/middleware/config/rig.json | 5 - server/middleware/jest.config.js | 7 - server/middleware/package.json | 47 - server/middleware/src/applyTx.ts | 138 -- server/middleware/src/broadcast.ts | 176 -- server/middleware/src/configuration.ts | 71 - server/middleware/src/contextName.ts | 82 - server/middleware/src/dbAdapter.ts | 91 - server/middleware/src/dbAdapterHelper.ts | 39 - server/middleware/src/derivedEntry.ts | 33 - server/middleware/src/domainFind.ts | 88 - server/middleware/src/domainTx.ts | 166 -- server/middleware/src/findSecurity.ts | 63 - server/middleware/src/fulltext.ts | 385 --- server/middleware/src/guestPermissions.ts | 109 - server/middleware/src/identity.ts | 76 - server/middleware/src/index.ts | 43 - server/middleware/src/liveQuery.ts | 98 - server/middleware/src/lookup.ts | 118 - server/middleware/src/lowLevel.ts | 91 - server/middleware/src/model.ts | 179 -- server/middleware/src/modified.ts | 62 - server/middleware/src/normalizeTx.ts | 259 -- server/middleware/src/pluginConfig.ts | 78 - server/middleware/src/private.ts | 171 -- server/middleware/src/queryJoin.ts | 157 -- server/middleware/src/queue.ts | 65 - server/middleware/src/spacePermissions.ts | 385 --- server/middleware/src/spaceSecurity.ts | 742 ------ .../middleware/src/tests/queryJoiner.spec.ts | 24 - server/middleware/src/triggers.ts | 542 ----- server/middleware/src/txPush.ts | 100 - server/middleware/src/userStatus.ts | 84 - server/middleware/src/utils.ts | 60 - server/middleware/tsconfig.json | 12 - server/minio/.eslintrc.js | 7 - server/minio/.npmignore | 4 - server/minio/config/rig.json | 5 - server/minio/jest.config.js | 7 - server/minio/package.json | 42 - server/minio/src/__tests__/minio.test.ts | 86 - server/minio/src/index.ts | 422 ---- server/minio/tsconfig.json | 12 - server/mongo/.eslintrc.js | 7 - server/mongo/.npmignore | 4 - server/mongo/CHANGELOG.json | 22 - server/mongo/CHANGELOG.md | 11 - server/mongo/config/rig.json | 5 - server/mongo/jest.config.js | 7 - server/mongo/package.json | 43 - server/mongo/src/__tests__/minmodel.ts | 234 -- server/mongo/src/__tests__/storage.test.ts | 327 --- server/mongo/src/__tests__/tasks.ts | 111 - server/mongo/src/index.ts | 40 - server/mongo/src/storage.ts | 1797 -------------- server/mongo/src/utils.ts | 231 -- server/mongo/tsconfig.json | 12 - server/postgres/.eslintrc.js | 7 - server/postgres/.npmignore | 4 - server/postgres/config/rig.json | 5 - server/postgres/jest.config.js | 7 - server/postgres/migrations/allSchema.sql | 37 - server/postgres/migrations/calendarSchema.sql | 5 - server/postgres/migrations/dncSchema.sql | 25 - server/postgres/migrations/eventSchema.sql | 19 - .../migrations/notificationSchema.sql | 25 - server/postgres/migrations/spaceSchema.sql | 5 - server/postgres/migrations/timeSchema.sql | 17 - server/postgres/migrations/txSchema.sql | 18 - server/postgres/migrations/uncSchema.sql | 11 - server/postgres/package.json | 43 - .../postgres/src/__tests__/conversion.spec.ts | 208 -- server/postgres/src/__tests__/minmodel.ts | 282 --- server/postgres/src/__tests__/storage.test.ts | 389 --- server/postgres/src/__tests__/tasks.ts | 121 - server/postgres/src/__tests__/utils.ts | 18 - server/postgres/src/index.ts | 64 - server/postgres/src/schemas.ts | 354 --- server/postgres/src/storage.ts | 2119 ----------------- server/postgres/src/types.ts | 1 - server/postgres/src/utils.ts | 505 ---- server/postgres/tsconfig.json | 12 - server/rpc/.eslintrc.js | 7 - server/rpc/.npmignore | 4 - server/rpc/config/rig.json | 4 - server/rpc/jest.config.js | 7 - server/rpc/package.json | 56 - server/rpc/src/index.ts | 18 - server/rpc/src/rpc.ts | 216 -- server/rpc/src/sliding.ts | 74 - server/rpc/src/test/rateLimit.spec.ts | 128 - server/rpc/tsconfig.json | 12 - server/s3/.eslintrc.js | 7 - server/s3/.npmignore | 4 - server/s3/config/rig.json | 5 - server/s3/jest.config.js | 7 - server/s3/package.json | 46 - server/s3/src/__tests__/s3.test.ts | 87 - server/s3/src/index.ts | 491 ---- server/s3/src/perfTest.ts | 106 - server/s3/tsconfig.json | 12 - server/server-pipeline/package.json | 2 +- server/server-pipeline/src/pipeline.ts | 31 +- server/server-storage/.eslintrc.js | 7 - server/server-storage/.npmignore | 4 - server/server-storage/config/rig.json | 4 - server/server-storage/jest.config.js | 7 - server/server-storage/package.json | 54 - server/server-storage/src/fallback.ts | 273 --- server/server-storage/src/index.ts | 18 - server/server-storage/src/readonly.ts | 98 - server/server-storage/src/starter.ts | 113 - .../src/tests/aggregator.spec.ts | 53 - .../server-storage/src/tests/memAdapters.ts | 148 -- .../src/tests/testConfig.spec.ts | 60 - server/server-storage/tsconfig.json | 12 - server/server/.eslintrc.js | 7 - server/server/.npmignore | 4 - server/server/CHANGELOG.json | 37 - server/server/CHANGELOG.md | 16 - server/server/config/rig.json | 5 - server/server/jest.config.js | 7 - server/server/package.json | 47 - server/server/src/blobs.ts | 199 -- server/server/src/client.ts | 456 ---- server/server/src/index.ts | 22 - server/server/src/sessionManager.ts | 1668 ------------- server/server/src/starter.ts | 75 - server/server/src/stats.ts | 75 - server/server/src/utils.ts | 91 - server/server/src/workspace.ts | 115 - server/server/tsconfig.json | 12 - 227 files changed, 999 insertions(+), 25164 deletions(-) create mode 100755 common/scripts/safe-publish.js delete mode 100644 server/client/.eslintrc.js delete mode 100644 server/client/.npmignore delete mode 100644 server/client/config/rig.json delete mode 100644 server/client/jest.config.js delete mode 100644 server/client/package.json delete mode 100644 server/client/src/account.ts delete mode 100644 server/client/src/blob.ts delete mode 100644 server/client/src/client.ts delete mode 100644 server/client/src/index.ts delete mode 100644 server/client/src/plugin.ts delete mode 100644 server/client/src/token.ts delete mode 100644 server/client/tsconfig.json delete mode 100644 server/collaboration/.eslintrc.js delete mode 100644 server/collaboration/.npmignore delete mode 100644 server/collaboration/config/rig.json delete mode 100644 server/collaboration/jest.config.js delete mode 100644 server/collaboration/package.json delete mode 100644 server/collaboration/src/index.ts delete mode 100644 server/collaboration/src/storage.ts delete mode 100644 server/collaboration/src/ydoc.ts delete mode 100644 server/collaboration/tsconfig.json delete mode 100644 server/core/.eslintrc.js delete mode 100644 server/core/.npmignore delete mode 100644 server/core/CHANGELOG.json delete mode 100644 server/core/CHANGELOG.md delete mode 100644 server/core/config/rig.json delete mode 100644 server/core/jest.config.js delete mode 100644 server/core/package.json delete mode 100644 server/core/src/adapter.ts delete mode 100644 server/core/src/base.ts delete mode 100644 server/core/src/benchmark/index.ts delete mode 100644 server/core/src/configuration.ts delete mode 100644 server/core/src/content.ts delete mode 100644 server/core/src/dbAdapterManager.ts delete mode 100644 server/core/src/domainHelper.ts delete mode 100644 server/core/src/index.ts delete mode 100644 server/core/src/limitter.ts delete mode 100644 server/core/src/mem.ts delete mode 100644 server/core/src/nullAdapter.ts delete mode 100644 server/core/src/pipeline.ts delete mode 100644 server/core/src/plugin.ts delete mode 100644 server/core/src/queue/dummyQueue.ts delete mode 100644 server/core/src/queue/index.ts delete mode 100644 server/core/src/queue/types.ts delete mode 100644 server/core/src/queue/users.ts delete mode 100644 server/core/src/queue/utils.ts delete mode 100644 server/core/src/queue/workspace.ts delete mode 100644 server/core/src/service.ts delete mode 100644 server/core/src/stats.ts delete mode 100644 server/core/src/storage.ts delete mode 100644 server/core/src/triggers.ts delete mode 100644 server/core/src/types.ts delete mode 100644 server/core/src/utils.ts delete mode 100644 server/core/tsconfig.json delete mode 100644 server/datalake/.eslintrc.js delete mode 100644 server/datalake/.npmignore delete mode 100644 server/datalake/config/rig.json delete mode 100644 server/datalake/jest.config.js delete mode 100644 server/datalake/package.json delete mode 100644 server/datalake/src/__tests__/utils.test.ts delete mode 100644 server/datalake/src/client.ts delete mode 100644 server/datalake/src/error.ts delete mode 100644 server/datalake/src/index.ts delete mode 100644 server/datalake/src/perfTest.ts delete mode 100644 server/datalake/src/utils.ts delete mode 100644 server/datalake/tsconfig.json delete mode 100644 server/elastic/.eslintrc.js delete mode 100644 server/elastic/.npmignore delete mode 100644 server/elastic/config/rig.json delete mode 100644 server/elastic/jest.config.js delete mode 100644 server/elastic/package.json delete mode 100644 server/elastic/src/__tests__/adapter.test.ts delete mode 100644 server/elastic/src/adapter.ts delete mode 100644 server/elastic/src/index.ts delete mode 100644 server/elastic/tsconfig.json delete mode 100644 server/kafka/.eslintrc.js delete mode 100644 server/kafka/.npmignore delete mode 100644 server/kafka/config/rig.json delete mode 100644 server/kafka/jest.config.js delete mode 100644 server/kafka/package.json delete mode 100644 server/kafka/src/__test__/queue.spec.ts delete mode 100644 server/kafka/src/index.ts delete mode 100644 server/kafka/tsconfig.json delete mode 100644 server/middleware/.eslintrc.js delete mode 100644 server/middleware/.npmignore delete mode 100644 server/middleware/config/rig.json delete mode 100644 server/middleware/jest.config.js delete mode 100644 server/middleware/package.json delete mode 100644 server/middleware/src/applyTx.ts delete mode 100644 server/middleware/src/broadcast.ts delete mode 100644 server/middleware/src/configuration.ts delete mode 100644 server/middleware/src/contextName.ts delete mode 100644 server/middleware/src/dbAdapter.ts delete mode 100644 server/middleware/src/dbAdapterHelper.ts delete mode 100644 server/middleware/src/derivedEntry.ts delete mode 100644 server/middleware/src/domainFind.ts delete mode 100644 server/middleware/src/domainTx.ts delete mode 100644 server/middleware/src/findSecurity.ts delete mode 100644 server/middleware/src/fulltext.ts delete mode 100644 server/middleware/src/guestPermissions.ts delete mode 100644 server/middleware/src/identity.ts delete mode 100644 server/middleware/src/index.ts delete mode 100644 server/middleware/src/liveQuery.ts delete mode 100644 server/middleware/src/lookup.ts delete mode 100644 server/middleware/src/lowLevel.ts delete mode 100644 server/middleware/src/model.ts delete mode 100644 server/middleware/src/modified.ts delete mode 100644 server/middleware/src/normalizeTx.ts delete mode 100644 server/middleware/src/pluginConfig.ts delete mode 100644 server/middleware/src/private.ts delete mode 100644 server/middleware/src/queryJoin.ts delete mode 100644 server/middleware/src/queue.ts delete mode 100644 server/middleware/src/spacePermissions.ts delete mode 100644 server/middleware/src/spaceSecurity.ts delete mode 100644 server/middleware/src/tests/queryJoiner.spec.ts delete mode 100644 server/middleware/src/triggers.ts delete mode 100644 server/middleware/src/txPush.ts delete mode 100644 server/middleware/src/userStatus.ts delete mode 100644 server/middleware/src/utils.ts delete mode 100644 server/middleware/tsconfig.json delete mode 100644 server/minio/.eslintrc.js delete mode 100644 server/minio/.npmignore delete mode 100644 server/minio/config/rig.json delete mode 100644 server/minio/jest.config.js delete mode 100644 server/minio/package.json delete mode 100644 server/minio/src/__tests__/minio.test.ts delete mode 100644 server/minio/src/index.ts delete mode 100644 server/minio/tsconfig.json delete mode 100644 server/mongo/.eslintrc.js delete mode 100644 server/mongo/.npmignore delete mode 100644 server/mongo/CHANGELOG.json delete mode 100644 server/mongo/CHANGELOG.md delete mode 100644 server/mongo/config/rig.json delete mode 100644 server/mongo/jest.config.js delete mode 100644 server/mongo/package.json delete mode 100644 server/mongo/src/__tests__/minmodel.ts delete mode 100644 server/mongo/src/__tests__/storage.test.ts delete mode 100644 server/mongo/src/__tests__/tasks.ts delete mode 100644 server/mongo/src/index.ts delete mode 100644 server/mongo/src/storage.ts delete mode 100644 server/mongo/src/utils.ts delete mode 100644 server/mongo/tsconfig.json delete mode 100644 server/postgres/.eslintrc.js delete mode 100644 server/postgres/.npmignore delete mode 100644 server/postgres/config/rig.json delete mode 100644 server/postgres/jest.config.js delete mode 100644 server/postgres/migrations/allSchema.sql delete mode 100644 server/postgres/migrations/calendarSchema.sql delete mode 100644 server/postgres/migrations/dncSchema.sql delete mode 100644 server/postgres/migrations/eventSchema.sql delete mode 100644 server/postgres/migrations/notificationSchema.sql delete mode 100644 server/postgres/migrations/spaceSchema.sql delete mode 100644 server/postgres/migrations/timeSchema.sql delete mode 100644 server/postgres/migrations/txSchema.sql delete mode 100644 server/postgres/migrations/uncSchema.sql delete mode 100644 server/postgres/package.json delete mode 100644 server/postgres/src/__tests__/conversion.spec.ts delete mode 100644 server/postgres/src/__tests__/minmodel.ts delete mode 100644 server/postgres/src/__tests__/storage.test.ts delete mode 100644 server/postgres/src/__tests__/tasks.ts delete mode 100644 server/postgres/src/__tests__/utils.ts delete mode 100644 server/postgres/src/index.ts delete mode 100644 server/postgres/src/schemas.ts delete mode 100644 server/postgres/src/storage.ts delete mode 100644 server/postgres/src/types.ts delete mode 100644 server/postgres/src/utils.ts delete mode 100644 server/postgres/tsconfig.json delete mode 100644 server/rpc/.eslintrc.js delete mode 100644 server/rpc/.npmignore delete mode 100644 server/rpc/config/rig.json delete mode 100644 server/rpc/jest.config.js delete mode 100644 server/rpc/package.json delete mode 100644 server/rpc/src/index.ts delete mode 100644 server/rpc/src/rpc.ts delete mode 100644 server/rpc/src/sliding.ts delete mode 100644 server/rpc/src/test/rateLimit.spec.ts delete mode 100644 server/rpc/tsconfig.json delete mode 100644 server/s3/.eslintrc.js delete mode 100644 server/s3/.npmignore delete mode 100644 server/s3/config/rig.json delete mode 100644 server/s3/jest.config.js delete mode 100644 server/s3/package.json delete mode 100644 server/s3/src/__tests__/s3.test.ts delete mode 100644 server/s3/src/index.ts delete mode 100644 server/s3/src/perfTest.ts delete mode 100644 server/s3/tsconfig.json delete mode 100644 server/server-storage/.eslintrc.js delete mode 100644 server/server-storage/.npmignore delete mode 100644 server/server-storage/config/rig.json delete mode 100644 server/server-storage/jest.config.js delete mode 100644 server/server-storage/package.json delete mode 100644 server/server-storage/src/fallback.ts delete mode 100644 server/server-storage/src/index.ts delete mode 100644 server/server-storage/src/readonly.ts delete mode 100644 server/server-storage/src/starter.ts delete mode 100644 server/server-storage/src/tests/aggregator.spec.ts delete mode 100644 server/server-storage/src/tests/memAdapters.ts delete mode 100644 server/server-storage/src/tests/testConfig.spec.ts delete mode 100644 server/server-storage/tsconfig.json delete mode 100644 server/server/.eslintrc.js delete mode 100644 server/server/.npmignore delete mode 100644 server/server/CHANGELOG.json delete mode 100644 server/server/CHANGELOG.md delete mode 100644 server/server/config/rig.json delete mode 100644 server/server/jest.config.js delete mode 100644 server/server/package.json delete mode 100644 server/server/src/blobs.ts delete mode 100644 server/server/src/client.ts delete mode 100644 server/server/src/index.ts delete mode 100644 server/server/src/sessionManager.ts delete mode 100644 server/server/src/starter.ts delete mode 100644 server/server/src/stats.ts delete mode 100644 server/server/src/utils.ts delete mode 100644 server/server/src/workspace.ts delete mode 100644 server/server/tsconfig.json diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 301077386c7..de4b44cecba 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -52,6 +52,9 @@ importers: '@hcengineering/client-resources': specifier: ^0.7.3 version: 0.7.3 + '@hcengineering/collaboration': + specifier: ^0.7.0 + version: 0.7.0(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/collaborator-client': specifier: ^0.7.3 version: 0.7.3 @@ -76,21 +79,39 @@ importers: '@hcengineering/core': specifier: ^0.7.3 version: 0.7.3 + '@hcengineering/datalake': + specifier: ^0.7.0 + version: 0.7.0 + '@hcengineering/elastic': + specifier: ^0.7.0 + version: 0.7.0 '@hcengineering/hulylake-client': specifier: ^0.7.3 version: 0.7.3 + '@hcengineering/kafka': + specifier: ^0.7.0 + version: 0.7.0 + '@hcengineering/middleware': + specifier: ^0.7.1 + version: 0.7.1 + '@hcengineering/minio': + specifier: ^0.7.0 + version: 0.7.0 '@hcengineering/model': specifier: ^0.7.3 version: 0.7.3 + '@hcengineering/mongo': + specifier: ^0.7.0 + version: 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': specifier: ^0.7.3 version: 0.7.3 '@hcengineering/platform-rig': specifier: ^0.7.10 version: 0.7.10 - '@hcengineering/postgres-base': - specifier: ^0.7.6 - version: 0.7.6 + '@hcengineering/postgres': + specifier: ^0.7.0 + version: 0.7.0 '@hcengineering/query': specifier: ^0.7.3 version: 0.7.3 @@ -103,6 +124,21 @@ importers: '@hcengineering/rpc': specifier: ^0.7.3 version: 0.7.3 + '@hcengineering/s3': + specifier: ^0.7.0 + version: 0.7.0 + '@hcengineering/server': + specifier: ^0.7.0 + version: 0.7.0 + '@hcengineering/server-client': + specifier: ^0.7.0 + version: 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': + specifier: ^0.7.0 + version: 0.7.0 + '@hcengineering/server-storage': + specifier: ^0.7.0 + version: 0.7.0 '@hcengineering/server-token': specifier: ^0.7.0 version: 0.7.2 @@ -225,7 +261,7 @@ importers: version: file:projects/backup-api-pod.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13) '@rush-temp/backup-service': specifier: file:./projects/backup-service.tgz - version: file:projects/backup-service.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + version: file:projects/backup-service.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4) '@rush-temp/billing': specifier: file:./projects/billing.tgz version: file:projects/billing.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -292,9 +328,6 @@ importers: '@rush-temp/chunter-resources': specifier: file:./projects/chunter-resources.tgz version: file:projects/chunter-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/collaboration': - specifier: file:./projects/collaboration.tgz - version: file:projects/collaboration.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/collaborator': specifier: file:./projects/collaborator.tgz version: file:projects/collaborator.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(@tiptap/pm@2.11.7)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(y-protocols@1.0.6(yjs@13.6.27)) @@ -325,9 +358,6 @@ importers: '@rush-temp/controlled-documents-resources': specifier: file:./projects/controlled-documents-resources.tgz version: file:projects/controlled-documents-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@tiptap/pm@2.11.7)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/datalake': - specifier: file:./projects/datalake.tgz - version: file:projects/datalake.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9) '@rush-temp/desktop': specifier: file:./projects/desktop.tgz version: file:projects/desktop.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(sass@1.71.1)(utf-8-validate@6.0.4) @@ -385,9 +415,6 @@ importers: '@rush-temp/drive-resources': specifier: file:./projects/drive-resources.tgz version: file:projects/drive-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/elastic': - specifier: file:./projects/elastic.tgz - version: file:projects/elastic.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9) '@rush-temp/emoji': specifier: file:./projects/emoji.tgz version: file:projects/emoji.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -408,7 +435,7 @@ importers: version: file:projects/export-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/front': specifier: file:./projects/front.tgz - version: file:projects/front.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9) + version: file:projects/front.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@rush-temp/github': specifier: file:./projects/github.tgz version: file:projects/github.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -471,7 +498,7 @@ importers: version: file:projects/image-cropper-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/import-tool': specifier: file:./projects/import-tool.tgz - version: file:projects/import-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9)) + version: file:projects/import-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@rush-temp/importer': specifier: file:./projects/importer.tgz version: file:projects/importer.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -487,9 +514,6 @@ importers: '@rush-temp/inventory-resources': specifier: file:./projects/inventory-resources.tgz version: file:projects/inventory-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/kafka': - specifier: file:./projects/kafka.tgz - version: file:projects/kafka.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/kanban': specifier: file:./projects/kanban.tgz version: file:projects/kanban.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -541,12 +565,6 @@ importers: '@rush-temp/media-resources': specifier: file:./projects/media-resources.tgz version: file:projects/media-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/middleware': - specifier: file:./projects/middleware.tgz - version: file:projects/middleware.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/minio': - specifier: file:./projects/minio.tgz - version: file:projects/minio.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/model-achievement': specifier: file:./projects/model-achievement.tgz version: file:projects/model-achievement.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -597,7 +615,7 @@ importers: version: file:projects/model-contact.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/model-controlled-documents': specifier: file:./projects/model-controlled-documents.tgz - version: file:projects/model-controlled-documents.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + version: file:projects/model-controlled-documents.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/model-core': specifier: file:./projects/model-core.tgz version: file:projects/model-core.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -609,7 +627,7 @@ importers: version: file:projects/model-desktop-preferences.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/model-document': specifier: file:./projects/model-document.tgz - version: file:projects/model-document.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + version: file:projects/model-document.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/model-drive': specifier: file:./projects/model-drive.tgz version: file:projects/model-drive.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -823,9 +841,6 @@ importers: '@rush-temp/model-workbench': specifier: file:./projects/model-workbench.tgz version: file:projects/model-workbench.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/mongo': - specifier: file:./projects/mongo.tgz - version: file:projects/mongo.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/notification': specifier: file:./projects/notification.tgz version: file:projects/notification.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -858,13 +873,13 @@ importers: version: file:projects/pod-ai-bot.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(zod@3.24.2) '@rush-temp/pod-analytics-collector': specifier: file:./projects/pod-analytics-collector.tgz - version: file:projects/pod-analytics-collector.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13) + version: file:projects/pod-analytics-collector.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4) '@rush-temp/pod-backup': specifier: file:./projects/pod-backup.tgz - version: file:projects/pod-backup.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13) + version: file:projects/pod-backup.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@rush-temp/pod-billing': specifier: file:./projects/pod-billing.tgz - version: file:projects/pod-billing.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13) + version: file:projects/pod-billing.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4) '@rush-temp/pod-calendar': specifier: file:./projects/pod-calendar.tgz version: file:projects/pod-calendar.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(utf-8-validate@6.0.4) @@ -876,7 +891,7 @@ importers: version: file:projects/pod-collaborator.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13) '@rush-temp/pod-datalake': specifier: file:./projects/pod-datalake.tgz - version: file:projects/pod-datalake.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13) + version: file:projects/pod-datalake.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4) '@rush-temp/pod-export': specifier: file:./projects/pod-export.tgz version: file:projects/pod-export.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(utf-8-validate@6.0.4) @@ -888,7 +903,7 @@ importers: version: file:projects/pod-front.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13) '@rush-temp/pod-fulltext': specifier: file:./projects/pod-fulltext.tgz - version: file:projects/pod-fulltext.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13) + version: file:projects/pod-fulltext.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4) '@rush-temp/pod-github': specifier: file:./projects/pod-github.tgz version: file:projects/pod-github.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) @@ -912,7 +927,7 @@ importers: version: file:projects/pod-notification.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9)) '@rush-temp/pod-preview': specifier: file:./projects/pod-preview.tgz - version: file:projects/pod-preview.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13) + version: file:projects/pod-preview.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4) '@rush-temp/pod-print': specifier: file:./projects/pod-print.tgz version: file:projects/pod-print.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4) @@ -921,10 +936,10 @@ importers: version: file:projects/pod-process.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(utf-8-validate@6.0.4) '@rush-temp/pod-server': specifier: file:./projects/pod-server.tgz - version: file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13) + version: file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.3) '@rush-temp/pod-sign': specifier: file:./projects/pod-sign.tgz - version: file:projects/pod-sign.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) + version: file:projects/pod-sign.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4) '@rush-temp/pod-stats': specifier: file:./projects/pod-stats.tgz version: file:projects/pod-stats.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13) @@ -943,9 +958,6 @@ importers: '@rush-temp/pod-workspace': specifier: file:./projects/pod-workspace.tgz version: file:projects/pod-workspace.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) - '@rush-temp/postgres': - specifier: file:./projects/postgres.tgz - version: file:projects/postgres.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/preference': specifier: file:./projects/preference.tgz version: file:projects/preference.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -996,7 +1008,7 @@ importers: version: file:projects/qms-desktop.tgz(webpack@5.97.1) '@rush-temp/qms-doc-import-tool': specifier: file:./projects/qms-doc-import-tool.tgz - version: file:projects/qms-doc-import-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9)) + version: file:projects/qms-doc-import-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@rush-temp/qms-tests-sanity': specifier: file:./projects/qms-tests-sanity.tgz version: file:projects/qms-tests-sanity.tgz @@ -1042,15 +1054,9 @@ importers: '@rush-temp/request-resources': specifier: file:./projects/request-resources.tgz version: file:projects/request-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/s3': - specifier: file:./projects/s3.tgz - version: file:projects/s3.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/scripts': specifier: file:./projects/scripts.tgz version: file:projects/scripts.tgz - '@rush-temp/server': - specifier: file:./projects/server.tgz - version: file:projects/server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/server-activity': specifier: file:./projects/server-activity.tgz version: file:projects/server-activity.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -1077,7 +1083,7 @@ importers: version: file:projects/server-attachment-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/server-backup': specifier: file:./projects/server-backup.tgz - version: file:projects/server-backup.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + version: file:projects/server-backup.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4) '@rush-temp/server-calendar': specifier: file:./projects/server-calendar.tgz version: file:projects/server-calendar.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -1096,9 +1102,6 @@ importers: '@rush-temp/server-chunter-resources': specifier: file:./projects/server-chunter-resources.tgz version: file:projects/server-chunter-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/server-client': - specifier: file:./projects/server-client.tgz - version: file:projects/server-client.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4) '@rush-temp/server-collaboration': specifier: file:./projects/server-collaboration.tgz version: file:projects/server-collaboration.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -1117,9 +1120,6 @@ importers: '@rush-temp/server-controlled-documents-resources': specifier: file:./projects/server-controlled-documents-resources.tgz version: file:projects/server-controlled-documents-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/server-core': - specifier: file:./projects/server-core.tgz - version: file:projects/server-core.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/server-document': specifier: file:./projects/server-document.tgz version: file:projects/server-document.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -1216,9 +1216,6 @@ importers: '@rush-temp/server-setting-resources': specifier: file:./projects/server-setting-resources.tgz version: file:projects/server-setting-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - '@rush-temp/server-storage': - specifier: file:./projects/server-storage.tgz - version: file:projects/server-storage.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9) '@rush-temp/server-tags': specifier: file:./projects/server-tags.tgz version: file:projects/server-tags.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -1248,7 +1245,7 @@ importers: version: file:projects/server-time-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/server-tool': specifier: file:./projects/server-tool.tgz - version: file:projects/server-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(gcp-metadata@5.3.0(encoding@0.1.13))(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + version: file:projects/server-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(gcp-metadata@5.3.0(encoding@0.1.13))(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4) '@rush-temp/server-tracker': specifier: file:./projects/server-tracker.tgz version: file:projects/server-tracker.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -1422,7 +1419,7 @@ importers: version: file:projects/workbench-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/workspace-service': specifier: file:./projects/workspace-service.tgz - version: file:projects/workspace-service.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9)) + version: file:projects/workspace-service.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4) '@signpdf/placeholder-pdf-lib': specifier: ^3.2.4 version: 3.2.4(pdf-lib@1.17.1) @@ -1711,9 +1708,6 @@ importers: autoprefixer: specifier: ^10.4.14 version: 10.4.17(postcss@8.5.3) - base64-js: - specifier: ^1.5.1 - version: 1.5.1 big-integer: specifier: ^1.6.51 version: 1.6.52 @@ -2029,9 +2023,6 @@ importers: mini-css-extract-plugin: specifier: ^2.2.0 version: 2.8.0(webpack@5.97.1) - minio: - specifier: ^8.0.5 - version: 8.0.5 mongodb: specifier: ^6.16.0 version: 6.16.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) @@ -2994,6 +2985,9 @@ packages: '@hcengineering/client@0.7.3': resolution: {integrity: sha512-kMK8WwK7b1ViZa9HP6xwyX5Es0dqHXCkBmoAtd1SEaU97sGnuUC/vYPcqDJtpm7y9pDhyFfwoHQAk8UYEa0wgQ==} + '@hcengineering/collaboration@0.7.0': + resolution: {integrity: sha512-V9SxR9dBsr+RxNUcIvpbncVkdrA4ST6CdjSH4SpudmuEfJY8Kk625DJmmqOntD+l7jy2VBM9uk58QmMiqVGwVQ==} + '@hcengineering/collaborator-client@0.7.3': resolution: {integrity: sha512-WBJtwGM+s2WPbUFbcNVlStf7j15ryqgQWXL+G1KpQhK8UCtS5ii5v0UqC4QxaaCC5Y0it4uKTHtJK/Sa/r/ZCw==} @@ -3024,18 +3018,36 @@ packages: '@hcengineering/core@0.7.3': resolution: {integrity: sha512-twB7xtB5gQR5cohRwsI9X5V9360T7pQTvrcQBva2dg9GVKw/mWPhTqYe3zADp0nOEhiygLB3W2e1+e/92NwzKQ==} + '@hcengineering/datalake@0.7.0': + resolution: {integrity: sha512-Puvkb49nOhapth1PUgedKZTY9dXtYDeVeLngtt7r2zI0v13642YXGDs3v+uZvvlKfmtvN3p2PY0D1GX6GoW8LQ==} + + '@hcengineering/elastic@0.7.0': + resolution: {integrity: sha512-VsVLZgzdNRZCvYAYPNi4mngGl4kgcTaO6rew0lC/rC9CyrkXkRIsB9r56ynw7ab0upzcOGVtT9ejUhTWmUY33w==} + '@hcengineering/hulylake-client@0.7.3': resolution: {integrity: sha512-3q/ZUBlTEsCbd2xg2Vmni1yEu6Sq6Q2K5FJY73tY/XezjQeT5bL3Sw4kq+Iiwp7Hgp4KFxXFC+cnbRqP+Zz0Gw==} + '@hcengineering/kafka@0.7.0': + resolution: {integrity: sha512-Aw1NyeoIy79NvL4P4Hy6wuODdR7Bkc/rU2DsEez/V26oc7T1zL6s6oucAL80q+3P2Cz3Yw1tDLWjoTV5V8gf6A==} + '@hcengineering/measurements-otlp@0.7.10': resolution: {integrity: sha512-0V/s8ofZe7UyDndnFARZGpwzDFWPB9dF5z+SQkJRSjRXSFNR5WRU/1EEAOsRf51Y00kbYP5Wi/nQv58J29p7vA==} '@hcengineering/measurements@0.7.10': resolution: {integrity: sha512-6xsrHorpJkQth4GVtdPoCM5s6USyYtoV9j8tCPhbLUk5yQn+CIvrDZxN60JGBOqvL6hYb6cis2FrAG5NC3HYPw==} + '@hcengineering/middleware@0.7.1': + resolution: {integrity: sha512-36AhtomB1H0mgG34zqtLwfoLL2m2LIq61i9ZXLiE4kjOWUYxaszLQCHRgp3HaLvGGIealx0plZWpzQanL11sGg==} + + '@hcengineering/minio@0.7.0': + resolution: {integrity: sha512-7J8N2O3wX0A/gD82QouGBSIzHrI3gNEJ8qXBrmAjdSZKxBJg+FTvkR6wx9NE0Mu2DQHpzsZITBRJW4auoibOmQ==} + '@hcengineering/model@0.7.3': resolution: {integrity: sha512-tQXFxZERQlhobPQXRzGUclDtHRU00VzLUZRBKFd9RG1hmWNH6mQS64Ywn/lhGnn4iGvy+ntfB2vtFLa1FaeSeQ==} + '@hcengineering/mongo@0.7.0': + resolution: {integrity: sha512-srpMoKlBWDy4Ad6fi68qBKXfUyxhD5L5SOM+Q/rQ3eAdhUqSSx9sdQ8cFybratL/immBgl0FT58I8BABZirL6A==} + '@hcengineering/platform-rig@0.7.10': resolution: {integrity: sha512-KQb4ArPlQL05lctvjbL6GeW0mi51wJZRWjB1FO4FMHREXITnNhHZGV3TF8m1HzK9be0BgNWM7FbNwYRdwEsuZg==} hasBin: true @@ -3046,6 +3058,9 @@ packages: '@hcengineering/postgres-base@0.7.6': resolution: {integrity: sha512-6Vc1uTkPYj3iZV2AwFlG6k0FyQ5kwMujV/INn7wM6EnjdcAZ90F313+69YnkEpbUT5xBc8PetDXyaRx+uTye1w==} + '@hcengineering/postgres@0.7.0': + resolution: {integrity: sha512-jeNnDuWEO4uN77yiNfiMwcSWPY+MZ0at8J/uWWLhElV2QQZtyM7ckA4jFE03gBXa7isITb4yBB2G5en4pjd/dA==} + '@hcengineering/query@0.7.3': resolution: {integrity: sha512-rfKsi44gexHMUFUraofrDkJURUaYSoBqef4qKKPKPHM42VButYXypfBCom0EYX/c61vJyH5oCEoIWi2RY4UJBQ==} @@ -3058,9 +3073,24 @@ packages: '@hcengineering/rpc@0.7.3': resolution: {integrity: sha512-vyDU+bu1frT4zC3z+OWgsgUv8+U2EmYsLrMVSM0IIc28z4PTCIkW7rQ7oXmdsyZ2CjdrCt39I7ZdL65qBCVqiQ==} + '@hcengineering/s3@0.7.0': + resolution: {integrity: sha512-Y/G/Ro6c0NC6wqhr8b0OTRCruDseXAzx/oTAVldJ90TaZpLgmryigpn+feN5QgFpdK7UzFaQQ2CcIrOQaLVZYw==} + + '@hcengineering/server-client@0.7.0': + resolution: {integrity: sha512-0T3k1TyqSL6o+93NXxj1HncMD45yOAmC1BA+lPY7/1juqsyoCHFFHFsWg5daP0wxOxne5xu1VEpg1zFeRTmS2g==} + + '@hcengineering/server-core@0.7.0': + resolution: {integrity: sha512-kKupYqmWJ9mksUMRQLaNnycrtmvLJH2WC3CuCf+RKpfmnciQHXN5yAjfv/b/13PJTlPJUrRF3G/19UB+6QDNrw==} + + '@hcengineering/server-storage@0.7.0': + resolution: {integrity: sha512-rJZHpyJraH0LItgDU1EJfErSXWieiOf/f8WuyWmWX7vfrr7Gaj1D6jcX1hzenIgYJ0UlA3ZScideUlVQLa6nFg==} + '@hcengineering/server-token@0.7.2': resolution: {integrity: sha512-cRRc13NWLwMYqKAO9SwD0r7TuGCp5ZHUztT7t+g5keUoTFzAJWusCG11pwRYUAODedwE08TpjYq2u5ET5R8JWw==} + '@hcengineering/server@0.7.0': + resolution: {integrity: sha512-SVeKPQ6i1V/IjTJ+U7cmjk+ukbXjQrxIAM4+ppfGe5/cU66H7L6SuxKLTs/fIAF4kdEdcEl9F9YYjPEarWoB/A==} + '@hcengineering/storage@0.7.3': resolution: {integrity: sha512-sYwXs1WTQVn7GZeIW/MSICRnK9d3XOen5ZdFrTAV0pPWmUf12aY49/OqgkOw4+DWC2xM3OdS3tjGbf0HttkDDA==} @@ -4399,11 +4429,11 @@ packages: resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} '@rush-temp/account-service@file:projects/account-service.tgz': - resolution: {integrity: sha512-+SN83xhzYkgkApqgsbzIazBrmnNCSirBc7Eo5AGIbw6fbmiV/KmcqOng5cei7MKEVzCevoA5bKvw/L1Px8/lLg==, tarball: file:projects/account-service.tgz} + resolution: {integrity: sha512-T0W//h98c53m71MZVfa2OkPT7Waoypz5dCJG6jik5gHA6AyT6pOY+ut9iDFdhxvHN/zUlKaymP7+CsZYC+Y9JA==, tarball: file:projects/account-service.tgz} version: 0.0.0 '@rush-temp/account@file:projects/account.tgz': - resolution: {integrity: sha512-fyFNae6KeXjFTwIU5j0gCjBN+Tv2Scx0fR009tA7rLkMHFxRzOKMos6cIL+dEmyCUzlqFSbK67QhYYEaprOikA==, tarball: file:projects/account.tgz} + resolution: {integrity: sha512-tg6L7wDWOdB4ZghjUaBJgNYHYGK/Qo9/CFzCtk9BelhFpSoQhSmYq1FMNbRkMGpUybHipG6iG5XgIcVpwqZvPA==, tarball: file:projects/account.tgz} version: 0.0.0 '@rush-temp/achievement-assets@file:projects/achievement-assets.tgz': @@ -4447,7 +4477,7 @@ packages: version: 0.0.0 '@rush-temp/ai-bot@file:projects/ai-bot.tgz': - resolution: {integrity: sha512-d/ZVjyAwbNV43xlkE/pFQxubrGfZOHoZh2JI7ftQC9Mz9LA4XPiBpXh8uQPWd8hmgyRGe4ATWDOBV0sV/XZLdQ==, tarball: file:projects/ai-bot.tgz} + resolution: {integrity: sha512-1i8GUSasxaIC6ZoFP5yfIDcu/wt4s8x2wFpc4AH0G98TWhFzrdLC+wVw8AMlmDEOQTUZf3/s0SJBwUg1RsLpfw==, tarball: file:projects/ai-bot.tgz} version: 0.0.0 '@rush-temp/analytics-collector-assets@file:projects/analytics-collector-assets.tgz': @@ -4487,11 +4517,11 @@ packages: version: 0.0.0 '@rush-temp/backup-api-pod@file:projects/backup-api-pod.tgz': - resolution: {integrity: sha512-lH668sxv673wISXL+1MqgZZkCD0fY5WfRQKPggf+dIDHlqmD+in1hklN2tpI2tq2+MD71azV2clo1iQ9tWk96g==, tarball: file:projects/backup-api-pod.tgz} + resolution: {integrity: sha512-slkkpjebMiHkx3PzIHyaLMqvbGNDnzuvnbwu+841hb9Za1LMx2LY185Si5shDN9PiCZyI9MZogT8Qbzph0Nm+g==, tarball: file:projects/backup-api-pod.tgz} version: 0.0.0 '@rush-temp/backup-service@file:projects/backup-service.tgz': - resolution: {integrity: sha512-QpiGz+r8R0aQbF1r32ibo3/XOsot9iw6jga+tL3ss7aL28OAF1dZlTFIRn0gVXQKedW//tscVUDjfzlmhfmdCA==, tarball: file:projects/backup-service.tgz} + resolution: {integrity: sha512-bKYWIPNUabrsZYM+ovz2/RwpZ47hGvQoamNlrUFNqGvbiBBLuW4sxzUiITldZ00Vw7gSgTM9yZ/isrgBimyLWQ==, tarball: file:projects/backup-service.tgz} version: 0.0.0 '@rush-temp/billing-assets@file:projects/billing-assets.tgz': @@ -4582,12 +4612,8 @@ packages: resolution: {integrity: sha512-YfFzoVzDpRMUZW0cTf3s+43dgqs9mQjBoMfHKB8MdNpMONKdfrD6AkRfmBT2BpPmNRSzdHOP8/J1qSvbuRv32g==, tarball: file:projects/chunter.tgz} version: 0.0.0 - '@rush-temp/collaboration@file:projects/collaboration.tgz': - resolution: {integrity: sha512-DhjVh5gitAOCT11k2tET5lGG4BZGQrgTZh8bH2TuBYzueYmN0VwBlA/3ik2h8fqHEi8MXnaZPL5s9TtR2zksig==, tarball: file:projects/collaboration.tgz} - version: 0.0.0 - '@rush-temp/collaborator@file:projects/collaborator.tgz': - resolution: {integrity: sha512-DbgslnudpGMWZkdqDln4uz8TjMixjAcPLOBbJAo9JrKiKEciP+OnH+if10hjjzUCAMzmHLk2iIhPkGFfSbUQSg==, tarball: file:projects/collaborator.tgz} + resolution: {integrity: sha512-BhQRCKGaoYAayfP2YDttJu8gbo4S0VznFDG+4SPYjzulYBvBDzEd1krb647AePe4+yxTF++9oZbDWWsqDUnn3A==, tarball: file:projects/collaborator.tgz} version: 0.0.0 '@rush-temp/communication-assets@file:projects/communication-assets.tgz': @@ -4626,10 +4652,6 @@ packages: resolution: {integrity: sha512-lbomdMbtra8nfm1ZgLGxtpGvcRggSVu+X6fL5cwFMeJ1NsXHoQ4DskBj1MJ+NBeWZpfpleHqvwtx4K+BUyetUQ==, tarball: file:projects/controlled-documents.tgz} version: 0.0.0 - '@rush-temp/datalake@file:projects/datalake.tgz': - resolution: {integrity: sha512-0j7Ek/ToBD0bXyHLaazwZKEwN37cMGdkWjunEmMt4DdC73cIqvBCuZtHTDnaDUktiVH4/1JRXDKfJA3upgejiw==, tarball: file:projects/datalake.tgz} - version: 0.0.0 - '@rush-temp/desktop-1@file:projects/desktop-1.tgz': resolution: {integrity: sha512-GyPdBMWrhMAmcwhCPEp+/y6k/9vCU+xRrnQn8dEfnnOhhMRIWnYU9yvvU8VIgREf4TTHoZrFfljBzaMsGO+upQ==, tarball: file:projects/desktop-1.tgz} version: 0.0.0 @@ -4706,10 +4728,6 @@ packages: resolution: {integrity: sha512-8GSn4NuABdsZs5gbDaVTi+74Tf72ZcxJrhyRQ0uCgOSb4KyrpRz7KKaFTAIfMsOULIBTTYAPket8tGUO36J7cg==, tarball: file:projects/drive.tgz} version: 0.0.0 - '@rush-temp/elastic@file:projects/elastic.tgz': - resolution: {integrity: sha512-LocS5YI3eGOdTYm+hR5POqb2D/W1LAdI1NUXCITjauOAiWWAk1f4oC8y7/9hFMCpzNHbxO4DS7tC+wmPSyztaA==, tarball: file:projects/elastic.tgz} - version: 0.0.0 - '@rush-temp/emoji-assets@file:projects/emoji-assets.tgz': resolution: {integrity: sha512-aEUaV6TTjwh4kFzLr+0sxtOskMIC6CE4zjENPnuWST+VsWpFwbFwXdhixKJumUanPqkxJ+wmAtA8UVDOEHWVJg==, tarball: file:projects/emoji-assets.tgz} version: 0.0.0 @@ -4735,7 +4753,7 @@ packages: version: 0.0.0 '@rush-temp/front@file:projects/front.tgz': - resolution: {integrity: sha512-FA9+TtgHOIM+vCHGReFNMDBifYpAPGpnaob6I3q9PpTyS9DV1P9zj0B5r47X04JmoY0/O+XKKOB4djxcdU9kKA==, tarball: file:projects/front.tgz} + resolution: {integrity: sha512-w2gb0IsYLz8Ij5O0V6VxgAevCJVHgVtpQlOUZQWBYBgiUg9zyiKFb0Re10EdAWltMcO0QydU/8eYBuXlqPvLSQ==, tarball: file:projects/front.tgz} version: 0.0.0 '@rush-temp/github-assets@file:projects/github-assets.tgz': @@ -4819,11 +4837,11 @@ packages: version: 0.0.0 '@rush-temp/import-tool@file:projects/import-tool.tgz': - resolution: {integrity: sha512-W3ssfL5EUEgvzAnixg5EbYDJtMqIVoU8FuxXwfBoYvZyvD+Aq/gtDw1+Cdl6pOZhr8PCnTbtaUNRV1VZmfal6g==, tarball: file:projects/import-tool.tgz} + resolution: {integrity: sha512-jdC9r6kqzMI/bMBNF7zCh1S+zSAjitgOiy8KKRVTmBaNLuMIZNRZFjr9xpKL2yhQNgnnspHNcILorYfxUnm9bw==, tarball: file:projects/import-tool.tgz} version: 0.0.0 '@rush-temp/importer@file:projects/importer.tgz': - resolution: {integrity: sha512-PY1kewfs/S6wEhjKDpL8H5roPqTGTqfpfKvlzdHpJjY19N2YPBShlkVSRnOuk3ViUJMTSUyfsUrKHxMmmvJa3g==, tarball: file:projects/importer.tgz} + resolution: {integrity: sha512-UH1YP5GwzgFf9Y0cSOM1tcBL+Ivsc0mivCcsx5aBe219TlJKTEafWlJACNeBC7iVJZCTKT/YAMwzoima9pE2+A==, tarball: file:projects/importer.tgz} version: 0.0.0 '@rush-temp/integration-client@file:projects/integration-client.tgz': @@ -4842,10 +4860,6 @@ packages: resolution: {integrity: sha512-/LnVDCoIwh4ua8WT9jp97W8jAwg/8Al/KI0BIIRNouwLLH4wbAD55N6oHsxcuDrm7YZyjc4cE1tYw+uyWXRxlg==, tarball: file:projects/inventory.tgz} version: 0.0.0 - '@rush-temp/kafka@file:projects/kafka.tgz': - resolution: {integrity: sha512-kFrmCbr6jeYzzbpmk2+k2QQ19Z58RruAH6o5QiecBY/xZMMWVAXKKzIieovDsuZmSbBpL7yA+51VA3BImSCDcQ==, tarball: file:projects/kafka.tgz} - version: 0.0.0 - '@rush-temp/kanban@file:projects/kanban.tgz': resolution: {integrity: sha512-AfvXpNVh4+U7BZUkXEpK2NKrgh41c+MNIDytg8eYLhv544N7mGnSflgQzXZTMMXde/u2Whk/q1s+wH29hfA8lg==, tarball: file:projects/kanban.tgz} version: 0.0.0 @@ -4895,7 +4909,7 @@ packages: version: 0.0.0 '@rush-temp/mail-common@file:projects/mail-common.tgz': - resolution: {integrity: sha512-kjhDrSW9gh4Sg/Uf/smLNaWyygK3kM5a4Oa/agJEn8j3Qx65xSZUD01olaRyj0tXSBlb2qI2IiwOVjvcQnIcKg==, tarball: file:projects/mail-common.tgz} + resolution: {integrity: sha512-SXCQN2ys+4/tA2L/8tf6md+RnIEGmbz3eP+xFh4JrHJyMFa50sO6SzxYwX25A6jwcPRkAgCCrxx1ZywCP4bTPQ==, tarball: file:projects/mail-common.tgz} version: 0.0.0 '@rush-temp/mail@file:projects/mail.tgz': @@ -4914,14 +4928,6 @@ packages: resolution: {integrity: sha512-+D47g6AVSFLVA1pMJdOtR2/ZZP2ajgCIasXWGa0eIEPb5/LEOtHnSBiqoUTTu+WXvNirbY24MhAFWDFhuzoo1w==, tarball: file:projects/media.tgz} version: 0.0.0 - '@rush-temp/middleware@file:projects/middleware.tgz': - resolution: {integrity: sha512-rDEQH5SmbmWyQA4l6PeyGXdL9cXu/+Vlxyhaac1REqQzfX8+FiqHBhrOrhtHSkOV4bwGMPVR5bknJ90YyIht3Q==, tarball: file:projects/middleware.tgz} - version: 0.0.0 - - '@rush-temp/minio@file:projects/minio.tgz': - resolution: {integrity: sha512-yctfqlpDiT2PnmBK9SRqu+iO8v/f8g+wmzbn4VM76FjoEQui1IYRJBLCWPyE3/0Z63MaPReEIW2tkEbSLBxtBQ==, tarball: file:projects/minio.tgz} - version: 0.0.0 - '@rush-temp/model-achievement@file:projects/model-achievement.tgz': resolution: {integrity: sha512-lCE82AN39dXkZimktVRRu8CuhSl0PAnWqcLfORzz7uS7sC7eXfYE1tkB/e+HIOJFq9MjK5KS9p0p8DyIoUemLw==, tarball: file:projects/model-achievement.tgz} version: 0.0.0 @@ -4987,11 +4993,11 @@ packages: version: 0.0.0 '@rush-temp/model-controlled-documents@file:projects/model-controlled-documents.tgz': - resolution: {integrity: sha512-ojIEFdjRJ+B2TJYlm6GJDvty6/DnJMc0wCd0wjFMhTVmEGVgp3Tktxx8dmQp2MkMzTSSUcV9qpOSgLSQ7sFNuA==, tarball: file:projects/model-controlled-documents.tgz} + resolution: {integrity: sha512-8uOxivdEn3e7hZ6uO3SSctAkAKQt45v6Xz8lRBf/vkaYNvpFLZFyRr3OdCYZXZC7a/ZzgVsj+GOcRxhTOb62Ew==, tarball: file:projects/model-controlled-documents.tgz} version: 0.0.0 '@rush-temp/model-core@file:projects/model-core.tgz': - resolution: {integrity: sha512-765/4bVfNCZXodDWZSfdApgOnw5UOrDUEJ/t61AeMVUrjvP3/qkG4j9Y+u/BKnCgmHeSOh0qKCu+I2uW2XOO+A==, tarball: file:projects/model-core.tgz} + resolution: {integrity: sha512-v6GTFnre9ZKPVED0RA0sABbiGEcP3HR43MsDztkao1z8ZCefoboSjYlwn4lNYOY0Iztfy5xU89Gs16xr8dNF2A==, tarball: file:projects/model-core.tgz} version: 0.0.0 '@rush-temp/model-desktop-downloads@file:projects/model-desktop-downloads.tgz': @@ -5003,7 +5009,7 @@ packages: version: 0.0.0 '@rush-temp/model-document@file:projects/model-document.tgz': - resolution: {integrity: sha512-QEK186+d+34fRz82Sm3JFWA0JnjaF/t0zWiKzVaBBLzG/LefXJhMBagq+ZLfQjWs2UVlb9TewEGbdVBrtl+L5Q==, tarball: file:projects/model-document.tgz} + resolution: {integrity: sha512-lUD7B+lguHADmFoAsMeOY6rcb+gPgziOfXzYeTKoJMSu5XOXx1YI8IFfuDZpqt9nWyW8Fd4jh3iGLiSmzWMuBg==, tarball: file:projects/model-document.tgz} version: 0.0.0 '@rush-temp/model-drive@file:projects/model-drive.tgz': @@ -5103,131 +5109,131 @@ packages: version: 0.0.0 '@rush-temp/model-server-activity@file:projects/model-server-activity.tgz': - resolution: {integrity: sha512-gN/Bks2UCtTzW/k3Z+zAXUUTCsS2b/tlHBJI5c+kCW7u4IP1tGQamVCpgnc2RhtG66jTmMc6rO6mAZQTrRPMRg==, tarball: file:projects/model-server-activity.tgz} + resolution: {integrity: sha512-H6nVyf3ofZyQ4Y7G56o9HX4QvTiH8zeiNTy6hsi4CCgzSj32B7sJcol+9Gg+42K8D/6KEne/IcpKGwMpgUdTZA==, tarball: file:projects/model-server-activity.tgz} version: 0.0.0 '@rush-temp/model-server-ai-bot@file:projects/model-server-ai-bot.tgz': - resolution: {integrity: sha512-/QbRvfPGMpTXSM2nOX6WypSO+V122vGBVQf0xOJD7WiJ87wya+/vivBXi4mqRyILvbKRfCMq5RSuBcn83+2Cfg==, tarball: file:projects/model-server-ai-bot.tgz} + resolution: {integrity: sha512-ryehGFwEfF2Yu55g4utXNie3XbMmwIa+mC0OpA5U/vfzhHcXScu9Fdh31bu0NlIWIjx2iLHpOrnBlqElHDjIyw==, tarball: file:projects/model-server-ai-bot.tgz} version: 0.0.0 '@rush-temp/model-server-attachment@file:projects/model-server-attachment.tgz': - resolution: {integrity: sha512-1Auglfk0aZ/yAlqTmDTNOwgXipFoG8i4GJuVgSKMBQF7kh4Gy3lv27uW42DvdA0JnB2hUxaO8lCJM37OqhTODg==, tarball: file:projects/model-server-attachment.tgz} + resolution: {integrity: sha512-smI8pEcl+ui3/uYrWCpAMb78l5IPUcNCEBDk87GTSsB41X3pg23WaNnuiPKu7pcO929OYZM52TNZJuyu2rq1Gg==, tarball: file:projects/model-server-attachment.tgz} version: 0.0.0 '@rush-temp/model-server-calendar@file:projects/model-server-calendar.tgz': - resolution: {integrity: sha512-zXD7tpLhB0QA8MSSRGr+ga0U1eR9IohJ9SO/Al6TfbjZUjsh8ZX2Kmcf1YPPlHpjYn1FL6TAQMq7koeLXdhWUQ==, tarball: file:projects/model-server-calendar.tgz} + resolution: {integrity: sha512-2331Hwi7+2TDI3ZPlu56NQh/+WwQMs9QqbigsopYXrAscqmNaCbAT8Fk5uosHYlNrxYEgbLvmqnC8R7uuOCCxA==, tarball: file:projects/model-server-calendar.tgz} version: 0.0.0 '@rush-temp/model-server-card@file:projects/model-server-card.tgz': - resolution: {integrity: sha512-RP2q6CUGmNIcN1JkdBSTBrQp95ipb/foZiF9dJgM8BLi44Xg0JhjN9KSSyvJ55jB3YGQMhu2eQQN8VMakjDN+Q==, tarball: file:projects/model-server-card.tgz} + resolution: {integrity: sha512-swZfxNJ6oupg2Bgvl5nLhQ9YaT17ok/MKl6fv5EVv3qL+cJi5ml5EoWAo94VmYjBgqDAMai6QY6DryiBQWCCuA==, tarball: file:projects/model-server-card.tgz} version: 0.0.0 '@rush-temp/model-server-chunter@file:projects/model-server-chunter.tgz': - resolution: {integrity: sha512-TkCXJdMsuRYNrTHX0cEjnNk+ivSDtuVFKt3HfjZ2LUDLbfrKjp9hYKEnHQWLl4OCZJCxddnWQupXmzwT0ayYkg==, tarball: file:projects/model-server-chunter.tgz} + resolution: {integrity: sha512-vmL8gXGsd3kXT0gF67Sa54Z4mpp1G1DFNcEnfJIl6wsjxYoKfuoikgu/EpAfXbkdf/JwBgBOV4ZXvCwP7Qu2Pg==, tarball: file:projects/model-server-chunter.tgz} version: 0.0.0 '@rush-temp/model-server-collaboration@file:projects/model-server-collaboration.tgz': - resolution: {integrity: sha512-cvboEcCAcb+bwaebygNvG1jQxgTp7YI9Ap+8vQnkxDa4cgfSXLI53UI3Sgi8jolSufz6RJ4WbBN/H5tgVMthWQ==, tarball: file:projects/model-server-collaboration.tgz} + resolution: {integrity: sha512-PZcV8mmZGIQ1tbZCh5tRzoMqxvjLBjk5aj5oSICvOeqW3E09XDrvx/09hWLpLQ30JtHHk8k8g3wjjCs5FzLB7A==, tarball: file:projects/model-server-collaboration.tgz} version: 0.0.0 '@rush-temp/model-server-contact@file:projects/model-server-contact.tgz': - resolution: {integrity: sha512-dYuRQYgUDs3CEkZ9wdBlNbh488HH2o6zFzy1BhD/+noI37K6SWA4BXrjMU+esdAyXK5CerLAFOPqDFlPSBJDjw==, tarball: file:projects/model-server-contact.tgz} + resolution: {integrity: sha512-qnpyUbsOnLQJ9z8moSSTUN/XpMombv0Dd5KgO3xQDwK4+iO9ATz4tyymZkfqCnDfIXI6cVlhOThWdZrTmvv53w==, tarball: file:projects/model-server-contact.tgz} version: 0.0.0 '@rush-temp/model-server-controlled-documents@file:projects/model-server-controlled-documents.tgz': - resolution: {integrity: sha512-RqEoNBlhFcVMWc9U82hVZnI5Q/JlPG3VzpKSPzYAj0uNrNZhVNIRqdoFac3e26h5l0QPocU/OoCeEy0m/PCadQ==, tarball: file:projects/model-server-controlled-documents.tgz} + resolution: {integrity: sha512-63lv728YFeg/658HOMWI4a9Hx/a5AqhIW/rTJ/qkMBHJcnBx/KZ8oFm2RuXuYVBop3MD4CxlEERd/c7RbC7M/A==, tarball: file:projects/model-server-controlled-documents.tgz} version: 0.0.0 '@rush-temp/model-server-core@file:projects/model-server-core.tgz': - resolution: {integrity: sha512-dqcoUVXmeI3NSorbAtCsdxHhR142aOO2pP2xYXDbdMdL/JgY4zoUl7cKO/c5YxAv6KtwE97JGzWqpCjSEMa2IA==, tarball: file:projects/model-server-core.tgz} + resolution: {integrity: sha512-dT5RxcNg7uaFGyeFqDeKq/i4RBQbSqmsum3nyUyAsx31kVMqmaWhRqSy7pnPe6YslnkGFbMX8egU4SFJniQudg==, tarball: file:projects/model-server-core.tgz} version: 0.0.0 '@rush-temp/model-server-document@file:projects/model-server-document.tgz': - resolution: {integrity: sha512-WJfUx0S2OS7a0cooJsUF9NWA3jegM6JTlp2kLx8Z4CkkQXaLUvMeM7s2kPxq7EZUyaJs/bn1kAVZCw4hxDAYpw==, tarball: file:projects/model-server-document.tgz} + resolution: {integrity: sha512-9ILp/oGLH4yNCBEszLzLLhpIQ+qWP5XKQXKn0qaTxORed08LOFwYy1V6rlqT7VLxoEBvTBDTvNdrnetoTKKaWw==, tarball: file:projects/model-server-document.tgz} version: 0.0.0 '@rush-temp/model-server-drive@file:projects/model-server-drive.tgz': - resolution: {integrity: sha512-hvP8Asb9E8xnb6jIVWGsiB/PtcdSSFkE0eSSeZF8ouR2uY1sy6G0xJ1Tq5NKsiGiQyeCfCWIIfDkKhhx1eqFZA==, tarball: file:projects/model-server-drive.tgz} + resolution: {integrity: sha512-VHSepvZYQkAwmUK5me7CAvgBTN/HM6EqyVG6OpAomykJ/CDuJZ0csO3wtNrqXLGoPHqd2S2dxItibCpNmbCdcg==, tarball: file:projects/model-server-drive.tgz} version: 0.0.0 '@rush-temp/model-server-gmail@file:projects/model-server-gmail.tgz': - resolution: {integrity: sha512-W90jOhC+SGTSgV8JgraQpo3z4yg/g/PA7R6sH530xasmHbBn/g3OJOP1tO2EkixFnBnzWBNMP2tdyaGIwxrF3g==, tarball: file:projects/model-server-gmail.tgz} + resolution: {integrity: sha512-Es5C0BgkcHVlOjgu/QECXnEbXS2Y95++39xdzFfG3C0EBVGDVXzV628DYkNfNMFITkL9FyUKdpfjR7H3Ky4xzw==, tarball: file:projects/model-server-gmail.tgz} version: 0.0.0 '@rush-temp/model-server-guest@file:projects/model-server-guest.tgz': - resolution: {integrity: sha512-qKvVj8dtN8Om62K1RHivzmqaTPnMQ69DqKJzSQFkEkI5CUuS+5E9sYcUJxFZSaVyPj4U9t4dVjZ+wCwvhDqZkw==, tarball: file:projects/model-server-guest.tgz} + resolution: {integrity: sha512-Wx6ag1MRs3bwIleWA5s+L3ZOifsf5TpLntG8CJ2Tk4WdMni81yEBeEEyKCyoOTsOQosEAO/jz9qivAedRbPq+A==, tarball: file:projects/model-server-guest.tgz} version: 0.0.0 '@rush-temp/model-server-hr@file:projects/model-server-hr.tgz': - resolution: {integrity: sha512-sowXHNmeN4yRBSa1Osul/VBsXAxsVApIFowtJ7p3K4YQ1l+ao3tchJbPtQhqtbpd/OE/g75f48ZDQz6SZVa+Tg==, tarball: file:projects/model-server-hr.tgz} + resolution: {integrity: sha512-LPss4qH50Uwt8/BG9nGnb0e41GEkubsE53FIFiUtTulTvZjY40cr1ZI1PBlxaczh6z5i6X6Rsfazo97z54yOAg==, tarball: file:projects/model-server-hr.tgz} version: 0.0.0 '@rush-temp/model-server-inventory@file:projects/model-server-inventory.tgz': - resolution: {integrity: sha512-1N0qEH3/cqsms2N5EjHBCe+qYNWnpYYz7u6i61o0elfHGnf1CdESQyGwS/j2vkY31hbgpcqY1OQME+vkH/56KA==, tarball: file:projects/model-server-inventory.tgz} + resolution: {integrity: sha512-LizyonV7HlwK0CxMzBPpIjR5hixo9Uxf27IJxeo0n6F3yDR41kXRE1apKKlfkRFljS/KlJSAorW/6U2t7wC6lA==, tarball: file:projects/model-server-inventory.tgz} version: 0.0.0 '@rush-temp/model-server-lead@file:projects/model-server-lead.tgz': - resolution: {integrity: sha512-hjtHIAdO/q05crszxoxv5wNrQ+J02/LCtXHYbNTQbsll5IP8wC91EZHk4Y2SCOIcrbWAree5pdLw5bA+bXL3CQ==, tarball: file:projects/model-server-lead.tgz} + resolution: {integrity: sha512-eSmn75z01zIFi4fF2370HDssCScdUHAud4nbykdG0n8yhqbEeveSumBr81NMC+oRxajvX6LpzxQPARa5cQ64qA==, tarball: file:projects/model-server-lead.tgz} version: 0.0.0 '@rush-temp/model-server-love@file:projects/model-server-love.tgz': - resolution: {integrity: sha512-H1XFWnU+UFdm1Yqb/MPGOqNY/AcjlIHqOx67EejJF3tAtFttWTvl/sx6w+ShtShKpvL1dKH7ZmQARVfb9d64Uw==, tarball: file:projects/model-server-love.tgz} + resolution: {integrity: sha512-CdgWRwhDDNpYDH8p7+BPkEGU45uMI3NEXSC1UtfOTWAftL59zBSVRjQ+vxXfjNb4Hx34m+B42IAkjlP7a6dt8w==, tarball: file:projects/model-server-love.tgz} version: 0.0.0 '@rush-temp/model-server-notification@file:projects/model-server-notification.tgz': - resolution: {integrity: sha512-y/RqK5KKQnDWT+PRb5WFtwpYzp1Ik1CNz8ilBeXDub6Ro9ol9FdszRJhQa9dsqAeyGTXV1/nxKaqYuRCkDZXqg==, tarball: file:projects/model-server-notification.tgz} + resolution: {integrity: sha512-0P6TXL3Vh/iS53bGJ7ll57RdCCXi+4PiCE/+uDZ0Ie0aplt9/YzoxRGAuZtG67hXAEvQ/80bC7PcbsORQw6k1g==, tarball: file:projects/model-server-notification.tgz} version: 0.0.0 '@rush-temp/model-server-process@file:projects/model-server-process.tgz': - resolution: {integrity: sha512-uc7eVTs/VItifVDcL5zOvdsj+5KVWQLkWkeVqx9vg58GrJXD5CHRWB+OtxWjykV0FrRRUO3DDUQZ94kY0pw5LQ==, tarball: file:projects/model-server-process.tgz} + resolution: {integrity: sha512-QIt/8LAe4v1DrV7WW1DSrf0ZXQBNDttiVpXuE0J8PwBdL4tL8JGrX3xHJaoWA5nkAXxKcFF7aLgIPxU1y+lhbQ==, tarball: file:projects/model-server-process.tgz} version: 0.0.0 '@rush-temp/model-server-products@file:projects/model-server-products.tgz': - resolution: {integrity: sha512-8/EHjFXFsFx8bDmzgw8JsL/vJNQQlHnEKEYy2ZzjKxBksTSJv9w1sDUzUiEOBttrU9GbtXkEIwx1btSai6jBIw==, tarball: file:projects/model-server-products.tgz} + resolution: {integrity: sha512-HvMMcr3zQqfd1+wsE7DMmt9Qt2vX3FI+jp1Lo7yp6gVjKrGsZIM0qrh3a8St5uPBKj/jK3qII4W86rLbCMKx4A==, tarball: file:projects/model-server-products.tgz} version: 0.0.0 '@rush-temp/model-server-recruit@file:projects/model-server-recruit.tgz': - resolution: {integrity: sha512-dOvCTxANHfNu5+QOs6dRoSsc/+AAX9ryu+IKE2iTsFOQVMP9kpn9/JsM0bNaGkrxQe4N/TJtCBfdyRvAZXkhxA==, tarball: file:projects/model-server-recruit.tgz} + resolution: {integrity: sha512-Di3rRQRzdVRNTsMqyfHVjvpzPTm8b7EL2zsZ2SDsYG0EZQvcTL/q9WP6CXO8J8W4xOl4VMyghQ4KtEJbhKVb0A==, tarball: file:projects/model-server-recruit.tgz} version: 0.0.0 '@rush-temp/model-server-request@file:projects/model-server-request.tgz': - resolution: {integrity: sha512-Xr2jIR1ZbJtRl/hAClNLBvCeXTM2c0qELuCvK/xS5tGptOLzhO5mJLkdm+5F5IivcWVG3nAlN8fWqBFVeYxecg==, tarball: file:projects/model-server-request.tgz} + resolution: {integrity: sha512-uZHpSInw74VD9n3Lm9PHcPZ7bKK88O+wkWcf7IjhovsHTS0Y6F8fJn5s6CrfGY7as9uxgBXoKhkOA2smhZ+FWA==, tarball: file:projects/model-server-request.tgz} version: 0.0.0 '@rush-temp/model-server-setting@file:projects/model-server-setting.tgz': - resolution: {integrity: sha512-6B+4OwzPdJj2F3ppqXT0I6VvMH3FnZ8YqPQz70NCrRdZFUGhY2OiB8gxLY8PT6JD28D/aQi1utgaIuI/udH+AQ==, tarball: file:projects/model-server-setting.tgz} + resolution: {integrity: sha512-PXg1EK+SzlHoXu+n3WhPpi9XyoEmmPCmvjYzhya+7KVlZke7kZNu3op054wGyoYE7rcFfOP1iBQIyPPXRKzcGQ==, tarball: file:projects/model-server-setting.tgz} version: 0.0.0 '@rush-temp/model-server-tags@file:projects/model-server-tags.tgz': - resolution: {integrity: sha512-7GRuQEX0MPi8S4O0jAQ5UHbHwIJBoL2Jmivl4UikHv0RTbrlZtyLk8hE7NCHfNmdbUe5F1E1VW8YvseoyKq8fA==, tarball: file:projects/model-server-tags.tgz} + resolution: {integrity: sha512-xfHyMVAHRmOamColHmZGBYNzpQ2zQrKMU/eoqjWorZoiGlfzCb5KE4k73WtOn+KhiATqPq4qzeSr5bL5FOVHBQ==, tarball: file:projects/model-server-tags.tgz} version: 0.0.0 '@rush-temp/model-server-task@file:projects/model-server-task.tgz': - resolution: {integrity: sha512-8y/GZgbVkRAy20k5WtceeCRkls8OkX+LETbb5NP8bwtKugxM8C5GmkXNnP9OEjQDGkREkpg5BRxNJzkLFhKcOQ==, tarball: file:projects/model-server-task.tgz} + resolution: {integrity: sha512-eYkD5mbOSbQx/UPLWdh9/P7n1H7/BSTfXnr1JK9F1ZH4rUuMxKcX6th67k+LwNSIi8SevNsJ1a7l2+akxPc8EA==, tarball: file:projects/model-server-task.tgz} version: 0.0.0 '@rush-temp/model-server-telegram@file:projects/model-server-telegram.tgz': - resolution: {integrity: sha512-DvccxyUg2B9MoBu0v3VZaINmjbc7rytzUgHMJ7B0Ac7gZBpIzBMtsV/g6iQaOB3pACTBJUsue0Fkxg8ei+A/Sg==, tarball: file:projects/model-server-telegram.tgz} + resolution: {integrity: sha512-ukGMeCEzgAuCV//qzcSs5Yk22f8N8VTmLbkwcuLjZMbeuhzCIcv+z8W1es/K8xshYDB8t8WgEdt4KXz/9Rah3Q==, tarball: file:projects/model-server-telegram.tgz} version: 0.0.0 '@rush-temp/model-server-templates@file:projects/model-server-templates.tgz': - resolution: {integrity: sha512-lRDXqE7rG16oZIIpjJ0Tmcrn5Yv0Ps5fT+qNmnkPnd4cCNiFWPwU1hqsc2qFBJCw0ndo+MA1UV18qHqKCBLfnw==, tarball: file:projects/model-server-templates.tgz} + resolution: {integrity: sha512-kKDzOPrk0EuNnWpAofjHxM5t+yY5+TkBpwFKuubFFEifC7OEQgksImoWrNnCA8/vLSTW4csix9DoInbFnKSwEw==, tarball: file:projects/model-server-templates.tgz} version: 0.0.0 '@rush-temp/model-server-time@file:projects/model-server-time.tgz': - resolution: {integrity: sha512-ARL+SsHZSwCGyIZvjo+EPTEGxKupcAR4PSEPtc6MkJQoLjTR0zX2quxatPpnNbm0t3ccs/wwJL+DuB5BZI6gVQ==, tarball: file:projects/model-server-time.tgz} + resolution: {integrity: sha512-gecbXjw0QUbKIB3/UW2zoMZaxkDc24vQvigYsytF2P0PO42EdGskbsSBhMrNfuS2inccFULEvEURb8KN24t3/Q==, tarball: file:projects/model-server-time.tgz} version: 0.0.0 '@rush-temp/model-server-tracker@file:projects/model-server-tracker.tgz': - resolution: {integrity: sha512-4LyAQnDmPhJt36o9rovU8yT3gnMOSGf4k0P8R0qqUu35aaEIs8+KCyJxZ8JsMah+awkg8okNav/pPOSDKa4zFA==, tarball: file:projects/model-server-tracker.tgz} + resolution: {integrity: sha512-XNj0m76liUhPWPIBtuiOI5UitPA6IqPRR0+RpmCp1/UNz2JdNTcxfjtcMwHC47ke4K0SkmYpNiZLfIkDnOXKUg==, tarball: file:projects/model-server-tracker.tgz} version: 0.0.0 '@rush-temp/model-server-training@file:projects/model-server-training.tgz': - resolution: {integrity: sha512-j8JoscFdqU89H6VBQyj8vSUAZWMJ6iISDQZ/8OhT7aBgsk/cdNeb1rzmAE2bn/isUAB03+wl/7ZvlTt1gDhmZg==, tarball: file:projects/model-server-training.tgz} + resolution: {integrity: sha512-+vzcBzMg8JmVD21ZZSyXv020SZFDL4jGYQvijkkeRwhrOSHKSmNyhRb0cVDyeUyBsdublGnkyoc8HSJVcAgXTw==, tarball: file:projects/model-server-training.tgz} version: 0.0.0 '@rush-temp/model-server-view@file:projects/model-server-view.tgz': - resolution: {integrity: sha512-yycAZbZoXO2kCuoCHAZ8x7BREL/pN2n0rnWOwiRMX7/gok95jTtQKMnSWYMgoh53Z5GaNexcqN1osb1fm09w7A==, tarball: file:projects/model-server-view.tgz} + resolution: {integrity: sha512-XLSXhGCOdmx9+DtLYclRJefKVJM1D4xn6urWHKb6wqg1jxw+TbI7b7z9Ny1Tp0G+jfr9eHIp0hd/l2LHOnIaFA==, tarball: file:projects/model-server-view.tgz} version: 0.0.0 '@rush-temp/model-setting@file:projects/model-setting.tgz': @@ -5290,10 +5296,6 @@ packages: resolution: {integrity: sha512-X50CohHUDJhbFTP/pnIRZfNkO3KneGODoxGYB3fslMQJ3yxPjXTCEvlRG3NubRTT2CgDMLTU4FooRlygnuzVfQ==, tarball: file:projects/model-workbench.tgz} version: 0.0.0 - '@rush-temp/mongo@file:projects/mongo.tgz': - resolution: {integrity: sha512-5MOCo9Xp1a7HTYfhcNqdCg/BW3Y1186Bj5y9pZ3RRbr4NM0kk3aFRe10ZEUghLvkfDfFq8r8qQ4ItYfr8yv4vg==, tarball: file:projects/mongo.tgz} - version: 0.0.0 - '@rush-temp/notification-assets@file:projects/notification-assets.tgz': resolution: {integrity: sha512-aK1Vj57zbHR4Q4vB12XfadmAXLH6m0syb/pk6iJkXDTIC2ftsk1Y60KrIQfBTL9JW5B2Bm86NNl4LDH8V2jZkw==, tarball: file:projects/notification-assets.tgz} version: 0.0.0 @@ -5327,43 +5329,43 @@ packages: version: 0.0.0 '@rush-temp/pod-account@file:projects/pod-account.tgz': - resolution: {integrity: sha512-WmSftSits1g9F/DvQ7G6MnbCRcH3bii2FmwgyzytYLMYDT5ijDzJBI2T27WxsiI9F4gw2XUlP7oC4WpSXWTITw==, tarball: file:projects/pod-account.tgz} + resolution: {integrity: sha512-F7HC5vG9iwTTL2R7F4QcV0gLGnH7DAF+WoOKBIX4dsZYBvMulY0D+0k0yKyQeEgomu7aU6CVj0zJynVvnfta+A==, tarball: file:projects/pod-account.tgz} version: 0.0.0 '@rush-temp/pod-ai-bot@file:projects/pod-ai-bot.tgz': - resolution: {integrity: sha512-6U7v3wwUdBGzLCN8bjs9ww0ywKlU9/GJzDRqL4VrVgbup3IsgwzYGCHDf0xYFeUXxeKE2p8DHQFF51Y7apSxZQ==, tarball: file:projects/pod-ai-bot.tgz} + resolution: {integrity: sha512-1wROuGfqgHoOAX0+VQ46Di9AwgUJ/P4DESSw6AQE/4TpjRh3tDww2mdIkk9I3kWCHVn/cUGMQCt5AasJqELWLQ==, tarball: file:projects/pod-ai-bot.tgz} version: 0.0.0 '@rush-temp/pod-analytics-collector@file:projects/pod-analytics-collector.tgz': - resolution: {integrity: sha512-UaPYEmGUFinroOlwFXGR/Tk0fDSTJRmZofbWrSthjRrptadCxIhAC5S4So3m/E5oPeDoEwYB8P7W/aRJdR1apw==, tarball: file:projects/pod-analytics-collector.tgz} + resolution: {integrity: sha512-4mIkBu+TFFx7BIxgXqMjqI9hM7UjnuBMjOhGvDruWXkmovNkDQNe2C+cuEQt5rYoEL1BU8aCMXeoaGLgxBx+Tw==, tarball: file:projects/pod-analytics-collector.tgz} version: 0.0.0 '@rush-temp/pod-backup@file:projects/pod-backup.tgz': - resolution: {integrity: sha512-TgFFcMgm/wBRwYOvP207hQT3543Jm1+mGlbdvOiotofTHt4ICdcnqG8IGs+u1LXTKPcMvQNpoFC2skmDbhUCAg==, tarball: file:projects/pod-backup.tgz} + resolution: {integrity: sha512-9jnUxiL+EUed0f/HSIC51Cljdsy90QEn6hNzn2Fz3oQpAoXL9CYliR8HkhMXbcQgKeR5J+AuWEegnPU8406OfA==, tarball: file:projects/pod-backup.tgz} version: 0.0.0 '@rush-temp/pod-billing@file:projects/pod-billing.tgz': - resolution: {integrity: sha512-H6btMajpvcn/zw3qJ3LMSqavoPWzkQ8a1AK7hxpJ6p4bLZq0zlM+5qCWPFLpr6VLmcgkYsXaargoaMaui8zSNA==, tarball: file:projects/pod-billing.tgz} + resolution: {integrity: sha512-nxCZCrOyUwH9CtzL1GSH6o+ROCedr8TsrS+HNeljZUzlPFX8pxp519mbNnS25uXKVpf8N/W7NgOLh3vBjinupw==, tarball: file:projects/pod-billing.tgz} version: 0.0.0 '@rush-temp/pod-calendar-mailer@file:projects/pod-calendar-mailer.tgz': - resolution: {integrity: sha512-3pGJTbB3Af4yvh0MK/iCXqG5QFhcqwEHrc724qEoG/Z6atbIjha17rGfD1+y0lYmLroff+dlkMCTrpKBeMY66w==, tarball: file:projects/pod-calendar-mailer.tgz} + resolution: {integrity: sha512-UHkVvbm8JA59vAU0LMa3GZTEeIhRpxxCUu5Ix9CZQRoPgIWeAUAKgc1cbcomWcd7sjvxQCPRBpD4gHH95mpg1Q==, tarball: file:projects/pod-calendar-mailer.tgz} version: 0.0.0 '@rush-temp/pod-calendar@file:projects/pod-calendar.tgz': - resolution: {integrity: sha512-92Nled2A12NkCm2yRsemTLfn3ConPSf1fRXwirFFkQmQEVoY7vYAl4ora4ronIqLDhJrowuGGQVPtpUuDy8LQQ==, tarball: file:projects/pod-calendar.tgz} + resolution: {integrity: sha512-AOmwCetJsWB7gofCHtUFDM59Lf6mEZBlaBZ3FlrIB6j5L1nnj/TYXzUUElE3G/3ehputZ0wwiXGsP96v3ROeiA==, tarball: file:projects/pod-calendar.tgz} version: 0.0.0 '@rush-temp/pod-collaborator@file:projects/pod-collaborator.tgz': - resolution: {integrity: sha512-l7w9X+IFfvxTVsOTeDX/FKo9DS7usCEubpmidXBPTpxSlsXfsLFAf6ME8H3ISQjmDFgY2gazecZ2TDTAvWk3SA==, tarball: file:projects/pod-collaborator.tgz} + resolution: {integrity: sha512-rpaeZkYAUTs6/TuQGbKYw8cFyhNBCTLokASS8cH9HBFptsJNP3D1J2KmDpdrm7xWzs2MAeTLiM1EfmtBwgYwgg==, tarball: file:projects/pod-collaborator.tgz} version: 0.0.0 '@rush-temp/pod-datalake@file:projects/pod-datalake.tgz': - resolution: {integrity: sha512-q70g3uYr33Y3tQmZo0FCs2KUbJ2sFkcRbDB6QxUy4Q094epwW5i1u8W9OaivZXNXUWQEB/l+nuCIohNlQkeO7g==, tarball: file:projects/pod-datalake.tgz} + resolution: {integrity: sha512-3ljFEl3gE7+WYz+GDv/6x3+NtWetmaCWsPTaTF4m0hOSHdKDOWE3leF1aLx4A8Br0WlClOwSwpsZtGFfW/FECQ==, tarball: file:projects/pod-datalake.tgz} version: 0.0.0 '@rush-temp/pod-export@file:projects/pod-export.tgz': - resolution: {integrity: sha512-8B3JnkKsmEt45IAnBGaWfd8cqGhS2eMiKL9kq0a7bNef4End7bPgLcy5KYPu3X+CRu2rQqZk+hst5I08vHhtFQ==, tarball: file:projects/pod-export.tgz} + resolution: {integrity: sha512-D3bPa1l7pNWEktaajO5r8g9x8mo6E19QZfWAKslrS0pciQesSL6/Um4dWsLWjzEPW20rYPZYM9AD17LAbciSOw==, tarball: file:projects/pod-export.tgz} version: 0.0.0 '@rush-temp/pod-external@file:projects/pod-external.tgz': @@ -5371,35 +5373,35 @@ packages: version: 0.0.0 '@rush-temp/pod-front@file:projects/pod-front.tgz': - resolution: {integrity: sha512-rXkMCd7S0hjiURiQp/jlONIePkP5rLCdS5oT6oJkjhLyTbvNwKwPsFK8FYZd1ikSo7rZSjMDJZpNgeGulOlk8Q==, tarball: file:projects/pod-front.tgz} + resolution: {integrity: sha512-gitxU7StszJJv8SNEQqEP+0p5hd5r29HXzWDYD/MHhzrbMfnW4MJFBJI7up0RHrtIRNmPlukJWV+2nzMuBrQlA==, tarball: file:projects/pod-front.tgz} version: 0.0.0 '@rush-temp/pod-fulltext@file:projects/pod-fulltext.tgz': - resolution: {integrity: sha512-ZdF6ubv5RKBJvXA7UYlkOl4AVJ4jnVxU26ERlimEcpDW4/HsQ+5f9HB1QhPP8R1STqpyGMSzV2g4hchzKLoaMA==, tarball: file:projects/pod-fulltext.tgz} + resolution: {integrity: sha512-aXpZB3ucKYsZE4nfhNIfIeUngzm5L2KR0AnfyWvrnxmETyK8S4uNHmj0E6E8yR5pOkaVtsUilGiE4mzJsrze1Q==, tarball: file:projects/pod-fulltext.tgz} version: 0.0.0 '@rush-temp/pod-github@file:projects/pod-github.tgz': - resolution: {integrity: sha512-j6yRnZt7274GElIntinY/65HeN3OgPZkyU7t6OxpZss6Nsp8chyGWhbVcTnQHGj5gyg/sRkSFMUm3R43640JHw==, tarball: file:projects/pod-github.tgz} + resolution: {integrity: sha512-mlgR8X7IJ7sIhGMFk38F662qmznOPdMZQrklxuyoFsCUpudRxHi2nyXO5yEBHBldJwRRlcLZOOddYUVlitgGCA==, tarball: file:projects/pod-github.tgz} version: 0.0.0 '@rush-temp/pod-gmail@file:projects/pod-gmail.tgz': - resolution: {integrity: sha512-0mwmS1kLXtJ5fSQd7bbpsibC6RKKGIjyevV98+afKqtDxacwko47oHK5fNSWLdqK7n3RGb2a0lSMYu6qayTFAA==, tarball: file:projects/pod-gmail.tgz} + resolution: {integrity: sha512-QqbEqudRWOC1Vp4hV5z1Suy1H2cX5SewA8jJcE7fkG27EBYuGylSUFN1DsK45m83sY/GdjhTwY1HBtKGqeBicg==, tarball: file:projects/pod-gmail.tgz} version: 0.0.0 '@rush-temp/pod-love@file:projects/pod-love.tgz': - resolution: {integrity: sha512-C2UeeIPMiBS1a62Aurz0yBDB81+64nwxd3mzdCk5kCTaAXQ6b7fri4lBgTrD6yBWCsg1nCTKCwPT2qf9U2q4sg==, tarball: file:projects/pod-love.tgz} + resolution: {integrity: sha512-qM/ARSxqlLwu9siwE4FvtOTcdsA3K7X3c2u+9Nckvk6Hy6MVDy8VzjlqNnFWjoSnW6j6aeyuyXppsvQZGsIglw==, tarball: file:projects/pod-love.tgz} version: 0.0.0 '@rush-temp/pod-mail-worker@file:projects/pod-mail-worker.tgz': - resolution: {integrity: sha512-S9RdkmGXu8FMO/rXTwIUGG+5ejazOLjmwwErSZyDQTF2jG7uyiDoRALJAZHepem8vs1V8d5NCvGY7rKzGESvjA==, tarball: file:projects/pod-mail-worker.tgz} + resolution: {integrity: sha512-63BeZDBqZg0LcTIigZmg0CDfPfejD+yEeSJ71NfQoRu+ISdL3fLx64zsBqpH+4pT3xqq4vxio2Xm3chsoE00+g==, tarball: file:projects/pod-mail-worker.tgz} version: 0.0.0 '@rush-temp/pod-mail@file:projects/pod-mail.tgz': - resolution: {integrity: sha512-cxk7CaTBVYaRRJDmOrASkze47H3QzRMSz4TC61ZgYH9mGlfFZcRGkkxyl+r+ErikfKPWDZ+xwoW0SCVkAyQYqQ==, tarball: file:projects/pod-mail.tgz} + resolution: {integrity: sha512-Lm0hNbSmfSkG2U65U+yYmK+8G1obFdPYrqpCapW3/mBIm1mcRmiRFa+7Jf114DtrOgn0aFKKse3oUfN6w5FiKQ==, tarball: file:projects/pod-mail.tgz} version: 0.0.0 '@rush-temp/pod-media@file:projects/pod-media.tgz': - resolution: {integrity: sha512-eTN42ufPPIcWxqK7ZeRHTdE6mJozxY2Cdx8I5T1IIDB7cUH1O976h+FOLLDukrhA4lV2euFoZaY5HhYZtsJWhQ==, tarball: file:projects/pod-media.tgz} + resolution: {integrity: sha512-AwnXNiJP45WUCqO4dwNBpV9h5sUB2KPg9B118u/LJUjqV3fpy79cj3l+K6MZrmYiXfYy0/MDHzgAKtOLgvgbDg==, tarball: file:projects/pod-media.tgz} version: 0.0.0 '@rush-temp/pod-notification@file:projects/pod-notification.tgz': @@ -5407,51 +5409,47 @@ packages: version: 0.0.0 '@rush-temp/pod-preview@file:projects/pod-preview.tgz': - resolution: {integrity: sha512-5aHFUqQFFRbq8PylX+WtTWfa0EBFLX81gq4/rjsbUWUYavhZDwVc7teTDTi3UVeLedPVA3r0ivIT/OoQ0rYGAQ==, tarball: file:projects/pod-preview.tgz} + resolution: {integrity: sha512-mKDDlPVZ3XsQ38KLTMaSZ7Pi7r+YY+8J+U9drDQ7E5sOg693jQYrrbLNY9+TK32UlNQB9peJj4KrGM5Om3+ZJQ==, tarball: file:projects/pod-preview.tgz} version: 0.0.0 '@rush-temp/pod-print@file:projects/pod-print.tgz': - resolution: {integrity: sha512-4aBWIpedN0utlPjfWq33lQwEUzkmJTLpHN0nXdosXj0HNOPguRXSrvKwoXlVphJFfMe8T349IbzH6gQnFHpBPw==, tarball: file:projects/pod-print.tgz} + resolution: {integrity: sha512-cWamsmnIiaLIenU5IlWryxJ190aeXz7m0fJwTnRP9+7aDWVNl6HuoTzmPRYJsJFcp64dFGPRniEODQfr20ycTA==, tarball: file:projects/pod-print.tgz} version: 0.0.0 '@rush-temp/pod-process@file:projects/pod-process.tgz': - resolution: {integrity: sha512-z6k+H9n/aqk3wjpGd6OgTrethenBhlbdy/ZjLaxgv4D4xFrN9W9I5peWbZjp2pAQozJm4G1SMeLqQS3pc1L39g==, tarball: file:projects/pod-process.tgz} + resolution: {integrity: sha512-bUUtITt9sr+d5HbZk8D0kp/jauxwHgA3gC4xliFmkF1YPHEzTYN+9cmSKAYidE/VZVl9R4a4E9ZCF7JvDAGp6A==, tarball: file:projects/pod-process.tgz} version: 0.0.0 '@rush-temp/pod-server@file:projects/pod-server.tgz': - resolution: {integrity: sha512-TxbZ/Y4m6YToXtkngHquC9rx2WwLb0nFwGWfV/pw0l0Cn/6kbxt3gVdqRzlVYMCjO3k8fn7YHc3xo7SlxemE1Q==, tarball: file:projects/pod-server.tgz} + resolution: {integrity: sha512-NKfk6U6zp5N4+icUWj3mhqAZqR0SeMmbghRBdatOMK6QziSvXPjENiD9RVlYsJy9ihtng9+VxEGvoo0WD4I8YQ==, tarball: file:projects/pod-server.tgz} version: 0.0.0 '@rush-temp/pod-sign@file:projects/pod-sign.tgz': - resolution: {integrity: sha512-KAlejm8ecTwMf1KF12BEiUDkJLLYcUo2KT0y93EWEjlPQQ4W/M3/XBsQtD6rbIFR/TLudiDtCBdujGsdGWMdqA==, tarball: file:projects/pod-sign.tgz} + resolution: {integrity: sha512-B1juOdDoRBlzOcumT3+AekTcgn8rgI905TxH+3ET51CTGDMfe5BQjs1W9zpoJ6X2p42+C7KXnQeOYtwfb4olBA==, tarball: file:projects/pod-sign.tgz} version: 0.0.0 '@rush-temp/pod-stats@file:projects/pod-stats.tgz': - resolution: {integrity: sha512-6EOQ20I9FV1Z654ifK2Asg6r6Uil3xtuBAt4V+hAHRiFIa2MH2vJ22wIulQHs3vqKFuzG+1DG2pEIzWIf0EeCA==, tarball: file:projects/pod-stats.tgz} + resolution: {integrity: sha512-z8GPnuTZIHQrs6HA9jtpSpfFuFRFGuHJQxfKO+Fosq8uyETbgdpu0V+n4CPQzuOS4H11Cr3+6lAW5uXcSyRndQ==, tarball: file:projects/pod-stats.tgz} version: 0.0.0 '@rush-temp/pod-telegram-bot@file:projects/pod-telegram-bot.tgz': - resolution: {integrity: sha512-JK/M2N0HByXVR1GeI/0VNU6PVYgU8ncQpjveA7VwS1a/fzlnRXgUKYrzHg1HVROStmIjVhY6qGsUMub57dUF+Q==, tarball: file:projects/pod-telegram-bot.tgz} + resolution: {integrity: sha512-aBfnA533w79Zvuf/yVqL/jCVNZTzVKLNIwbbXwl5Mam4b5qJsk6Rz1dH78pZDNxJmrQQQWcqtDAOTnjXGd+tTg==, tarball: file:projects/pod-telegram-bot.tgz} version: 0.0.0 '@rush-temp/pod-telegram@file:projects/pod-telegram.tgz': - resolution: {integrity: sha512-VJeZWzsOIQlaz2yGdbnuZ/8k6iyD+IUvLwv+rSnlSAqKBT/Xkf973zN2Fr7kHO1Fr8ivzgpgVZX7fvN6ZvbJpw==, tarball: file:projects/pod-telegram.tgz} + resolution: {integrity: sha512-ldBvwYsMBjT0Kf8CW/J4WDW7G3PdZoogf6J5MTngGevPNPdtGl9hW7u6ZnHRbaUjy2oVHbqwLsCVmMnp6tPJTA==, tarball: file:projects/pod-telegram.tgz} version: 0.0.0 '@rush-temp/pod-translate@file:projects/pod-translate.tgz': - resolution: {integrity: sha512-03Mf6T4lw9ttqF+UY+WhpcIO6dSTy6m6l2YcmsAE8cqtOrkaWOwyDj/m+T1uB44EHXECDkbUfvVHCazsFnp4sw==, tarball: file:projects/pod-translate.tgz} + resolution: {integrity: sha512-H4IY5HaeZxyLVAtFdNNYBTnSNt5HkstDH1nyiZNzXTkR8o5vDjDh2fA75R8KBEjFsvqKgNqmUW7auu6hzmnUPg==, tarball: file:projects/pod-translate.tgz} version: 0.0.0 '@rush-temp/pod-worker@file:projects/pod-worker.tgz': - resolution: {integrity: sha512-G+zIfCCtwQ3E6FfeeOmkwBIyqzzWRToEbJcBv854iUmZvsJ7orD7GyVYJaZgxxfrSEA327uCZnpg9g0NVGQ2kQ==, tarball: file:projects/pod-worker.tgz} + resolution: {integrity: sha512-fgjVXRXpRumviYNCqsuauj1ae6BO10N2OmyE2jqVP67FkeA295DVhWA9sVWcCN400/eQokoEeDBlbk1DCod//A==, tarball: file:projects/pod-worker.tgz} version: 0.0.0 '@rush-temp/pod-workspace@file:projects/pod-workspace.tgz': - resolution: {integrity: sha512-yxyeiRcboIo/6zkngZBvy9QWR7ICiKqAH8pwdyq2WMvYptmA4+sbmzEHFnuScJiV7m57F4UVUIdwTHLhwb1cbg==, tarball: file:projects/pod-workspace.tgz} - version: 0.0.0 - - '@rush-temp/postgres@file:projects/postgres.tgz': - resolution: {integrity: sha512-aW+Mqr+kn//zVX4TBB8LM2O44AIO6mYt3JY7JoFW3NeEN00a9CJfao4zg9+x0oL4Le2N9K+2H4CZZo0thiNQfg==, tarball: file:projects/postgres.tgz} + resolution: {integrity: sha512-UKz8j6ez5CObCKyHMCSnaca8hPcIiLBfYqDwt4LX+Q9BTLIQzLZNsDYCosxGiY0G5GF8S5lL8/GKVJbTK4xF3Q==, tarball: file:projects/pod-workspace.tgz} version: 0.0.0 '@rush-temp/preference-assets@file:projects/preference-assets.tgz': @@ -5519,7 +5517,7 @@ packages: version: 0.0.0 '@rush-temp/qms-doc-import-tool@file:projects/qms-doc-import-tool.tgz': - resolution: {integrity: sha512-Re/fMMRnh0A5v0E845Lg6w7x5uYc/L8LmEzHaX9oah6vlsbm3YPVxdzrNL4h0vq0dCOCVneum2O4vlKRzmpniQ==, tarball: file:projects/qms-doc-import-tool.tgz} + resolution: {integrity: sha512-oHJldMJMkfJgjgr7q4S7PTizkTrT+LIEYXZ9sJnhfHnlsts3GXgmKL2pF+4qs6oVWjvjPue1D3A6vxxjKuF9PQ==, tarball: file:projects/qms-doc-import-tool.tgz} version: 0.0.0 '@rush-temp/qms-tests-sanity@file:projects/qms-tests-sanity.tgz': @@ -5563,7 +5561,7 @@ packages: version: 0.0.0 '@rush-temp/rekoni-service@file:projects/rekoni-service.tgz': - resolution: {integrity: sha512-ETo2ah0XIP0tC9Z+Zc0a/hzQb+h7fmFjnFGPiaQuLbiPh3XNdvYIXdy4fB4DGVpQDTgo7D5zK4fp2uaxQ1TWAw==, tarball: file:projects/rekoni-service.tgz} + resolution: {integrity: sha512-lPzC/eLdfOUM5bYQrQItRtiv9oItiI5WrCLhW6Hv0W5fLHpzPdQA2Ajljdn412yVA8ZMYyn/ckXo96ecnhg4zw==, tarball: file:projects/rekoni-service.tgz} version: 0.0.0 '@rush-temp/rekoni@file:projects/rekoni.tgz': @@ -5582,164 +5580,152 @@ packages: resolution: {integrity: sha512-r6UvWnM7rMxSJLCEKZ8JMzXWUWMv8L6hQRH5wKKQcknLYTBytXBgxCR9OZsKSIBn1bljPiPWH84cTmjsGygTZw==, tarball: file:projects/request.tgz} version: 0.0.0 - '@rush-temp/s3@file:projects/s3.tgz': - resolution: {integrity: sha512-q1cXIgvE9DcI1OOTlKEMTSCHaMcRf93oGT8SyO62C/3IaTs4l9QFWjnvVZI4Xm943XAq5ZoJpCjl4Mh/UrdRaQ==, tarball: file:projects/s3.tgz} - version: 0.0.0 - '@rush-temp/scripts@file:projects/scripts.tgz': resolution: {integrity: sha512-w4Y4AfOHM7ccz3zkBOmuZzfvhBcGJNlz/IUsA60OtZakFhsDjnd4bYBwI0Xo7IhjUDIVIjDJTPrHY+je3AqcqQ==, tarball: file:projects/scripts.tgz} version: 0.0.0 '@rush-temp/server-activity-resources@file:projects/server-activity-resources.tgz': - resolution: {integrity: sha512-DGNguSDIyWlqvtdVHxSAZy6PQlXbNmnHzOVlAAugSOShdqfBVFV3VSKujKwvn4694Kdo1loCRFhWpYlhG7gB1g==, tarball: file:projects/server-activity-resources.tgz} + resolution: {integrity: sha512-Wwdhz8pNsiGTyEleYHU6gvjDozWKdMaKxVb/07WKN7vGPpqgeGdt0BwCgK3aaKfuO0741iBJH6TEUHOmGS4/OQ==, tarball: file:projects/server-activity-resources.tgz} version: 0.0.0 '@rush-temp/server-activity@file:projects/server-activity.tgz': - resolution: {integrity: sha512-9L9iCDtjKedlbDM74ZnwagwntKwNCMYI0tWKkBcIqA3BaUKZUI0EOTQF66V+TeoqTGBl/NtM2sQ4rI4UoWmvhQ==, tarball: file:projects/server-activity.tgz} + resolution: {integrity: sha512-oR5pfYp+TKldwgBPxtPDETLV86/L4r3+N7YJgzDsPRVrgXKanaThBmdcvlWdjgBzuRPz7z6CDo6qAyCzbJOiZA==, tarball: file:projects/server-activity.tgz} version: 0.0.0 '@rush-temp/server-ai-bot-resources@file:projects/server-ai-bot-resources.tgz': - resolution: {integrity: sha512-bPH5znCzYzglqnXx+H0vp8mp6xj+7IdwHdLdjXHPphBjpmEr0WUhG0BtwpTKXjAZlmtOY4A/ODLamxobNJAICg==, tarball: file:projects/server-ai-bot-resources.tgz} + resolution: {integrity: sha512-+8YJYpagVFYDX5XauS25lK8d9v3b7+UtuzOlN9vYH73lhSaziJ1z0pBvf76dV+o84cNwEUD7ljTAPpp8a70/aw==, tarball: file:projects/server-ai-bot-resources.tgz} version: 0.0.0 '@rush-temp/server-ai-bot@file:projects/server-ai-bot.tgz': - resolution: {integrity: sha512-We1+gsTvV3UTelKS54x6kCGMGS5bN26Vctu/PdfBNwSTmoNB+KfTj6XNCqkaJeSowlAJnm0lHxjy5p3+robYEw==, tarball: file:projects/server-ai-bot.tgz} + resolution: {integrity: sha512-on3LNW/G3GMu2/2P6XyrjiHWRBmEpcDmxPfGGSb0Wd9+4zDdIs3MEOFJ/CyAa0O6t2ei9fX0o9RyNm2CF3Juqg==, tarball: file:projects/server-ai-bot.tgz} version: 0.0.0 '@rush-temp/server-analytics-collector-resources@file:projects/server-analytics-collector-resources.tgz': - resolution: {integrity: sha512-fLgb+dNlXxykBmcrCyHTzDjqEcW4s8RuXeJ7vnboO6R5cKgH72ooL05l67730xmOSVkVOObYZcj4LSA5RMGFcQ==, tarball: file:projects/server-analytics-collector-resources.tgz} + resolution: {integrity: sha512-ewYptwVZB9AAQrBkpMg4u1J7PSKy8BVjlLyyIQeIMd6QN70AZ259WB6ckmT3xIfelJ8DPj5BfJRPNNLtw9wUjg==, tarball: file:projects/server-analytics-collector-resources.tgz} version: 0.0.0 '@rush-temp/server-analytics-collector@file:projects/server-analytics-collector.tgz': - resolution: {integrity: sha512-/HucUg33SaTFBQjDpWfv9AbU77aribPU17ZhQK1pIIj4c7VPSRjrC2h9M885l6v89a72aZh+p1V/cFbdkGjNRg==, tarball: file:projects/server-analytics-collector.tgz} + resolution: {integrity: sha512-MFTgN0ijEaA6Qeb30oAWFZh7V12OrGSmyWLZmXMWKgDWYmdF0Ser/D9CEUQF7rugisT3QJ3spv2dib6dxhzlyA==, tarball: file:projects/server-analytics-collector.tgz} version: 0.0.0 '@rush-temp/server-attachment-resources@file:projects/server-attachment-resources.tgz': - resolution: {integrity: sha512-grQV/cMDO3MG9noGDhp8drpSOqRePLBHYzRWUUhWeJH3dA0LXaxAw3kHfYk9tJso1FgKzCM/Xem71z/xD5e63w==, tarball: file:projects/server-attachment-resources.tgz} + resolution: {integrity: sha512-tBGouopPk48zYALpX7/hlC94ilVoEvm4NvhUe9NhCHpAo5RDImQGs+gj+xrlEKVk3RTfqawY9RmR1ri+Adrvrw==, tarball: file:projects/server-attachment-resources.tgz} version: 0.0.0 '@rush-temp/server-attachment@file:projects/server-attachment.tgz': - resolution: {integrity: sha512-3aSH7Ctb9sVKAMZ0KTnM/jKZp/X1/6gNYwyZDyCIRPMYI5f0DqCPkpdETnsrf8Ihqlqe9TF5kdiqG28pFaTlXg==, tarball: file:projects/server-attachment.tgz} + resolution: {integrity: sha512-ysq7AJHpTgMLxdv8NkIGAelhvbujd0vHt8HL2feqjBuhggfbF6lF8pvZgOjVVAie2Lwl98v9hil5Fqz0kBaqBQ==, tarball: file:projects/server-attachment.tgz} version: 0.0.0 '@rush-temp/server-backup@file:projects/server-backup.tgz': - resolution: {integrity: sha512-uX8hkvEBBkjjo1HASbybgWC6HuvNYyKLA2S9GvtFQuS9mFTcnDiQ/Pea/UCaC6M+2G2IfGmaZO4W5hhCPqwPmg==, tarball: file:projects/server-backup.tgz} + resolution: {integrity: sha512-yam/zsxs9VfHXC6iblnLyZ3Sggj/KLOZLLnu0bYffwhodP7XWBpvjVLYxBsiHbr7OFFvXTq9JaREytgkL+0GpQ==, tarball: file:projects/server-backup.tgz} version: 0.0.0 '@rush-temp/server-calendar-resources@file:projects/server-calendar-resources.tgz': - resolution: {integrity: sha512-wdvCPyEu3kWy42WzRuvVzDwcAQAeD73qP13F1M9BKIGdiZ6YnxEOs8J7vr/cpXilvck1kbHLV7E90I63laiBAg==, tarball: file:projects/server-calendar-resources.tgz} + resolution: {integrity: sha512-dBHJZ0Ec14eZ7Keh7NJ5LWd73ibWLmxubkCR8Cza1XVrKvoq2kIy9A7bqfvdWiFOrdDgzOyjW+Vs4AavV2wbGQ==, tarball: file:projects/server-calendar-resources.tgz} version: 0.0.0 '@rush-temp/server-calendar@file:projects/server-calendar.tgz': - resolution: {integrity: sha512-4HJpnLxGT9RXRG6tq+6fzolr1cEWFy2MZs9NTshuaD6kp84o+KhjRmPUGZGj35H562PXTUhJlwWa63KA8dg3mg==, tarball: file:projects/server-calendar.tgz} + resolution: {integrity: sha512-ZO8kyLaGFhwrqH9fuUvDf0FLQIvvpJuyP+Nfrf85uSWi/wKQsm9z3aEHqVpaDy+JaHsgdfzEsJf5UR+KsMO2kg==, tarball: file:projects/server-calendar.tgz} version: 0.0.0 '@rush-temp/server-card-resources@file:projects/server-card-resources.tgz': - resolution: {integrity: sha512-f+CkrzVHnDiX+L0f7XKBtCGc3GXdCVmIac7DATZrXFnLjzRBsoVN2MSbf43SjGXXmZ0r3mCvbb0GjlOUwgynng==, tarball: file:projects/server-card-resources.tgz} + resolution: {integrity: sha512-MVGsDVChdwEHwfBOiRlISVkQ/eHXvCObjiYeFEJDSQUu1VYEUiqeXjReVmmpPhDxePGYFFmVIt+QE8LOwylCMw==, tarball: file:projects/server-card-resources.tgz} version: 0.0.0 '@rush-temp/server-card@file:projects/server-card.tgz': - resolution: {integrity: sha512-6nL/txQ1IwQPNeWk1Cv3YHpqPxU8ajGFTT08cHunp2S/AyU4SJlXBqAzz3DeLXH3QS5p77eHoR3iWRzhTtxNvQ==, tarball: file:projects/server-card.tgz} + resolution: {integrity: sha512-ZqUe4goxZ/zN9th0clXONmZn3QsVIHx/at8/z3Ah4YWyEfXclU9yIpWk/OdTShuV53wDM1CQCM3v1SpRuLaGog==, tarball: file:projects/server-card.tgz} version: 0.0.0 '@rush-temp/server-chunter-resources@file:projects/server-chunter-resources.tgz': - resolution: {integrity: sha512-V8eP+NjIbKjKGGi44INbqmc2F9RAj6mlD7GtqM4Z1m+lzX8S3eGGayZsR/toP3GLwTys2vYMCcQf2ZQLQYjuqw==, tarball: file:projects/server-chunter-resources.tgz} + resolution: {integrity: sha512-zxewZ04zO7Aaj49UZcUkyHAf5SWq1ilUqm5UhH5o9I0WovJy3anQOed2/YHF56cs6WB/F72s26c42d4iWz+oGQ==, tarball: file:projects/server-chunter-resources.tgz} version: 0.0.0 '@rush-temp/server-chunter@file:projects/server-chunter.tgz': - resolution: {integrity: sha512-cbI8I+4UPH4BEv1w3JWWjlazKYW3c+m4wO3q3Q39k+RiKlgf7OtY3eIENg2IhySvL6jFt94FqA/6jvRO7fdQxQ==, tarball: file:projects/server-chunter.tgz} - version: 0.0.0 - - '@rush-temp/server-client@file:projects/server-client.tgz': - resolution: {integrity: sha512-XqDrMjXoC74HsX/OzkYOUiWtoWQ7Gwb00oxpIzdYgeE/Cn6xX/jVceJPnY+qjSPczKdJxyQAASYL6WeH+wP0OQ==, tarball: file:projects/server-client.tgz} + resolution: {integrity: sha512-vAmXsvJBBQT83Kq6nxHqo2C0R1+lkFVUOsw2knjcGcE5sUvoM/+u9yUDPTfHIscub3WZVb8DExjLIBx12y5tLw==, tarball: file:projects/server-chunter.tgz} version: 0.0.0 '@rush-temp/server-collaboration-resources@file:projects/server-collaboration-resources.tgz': - resolution: {integrity: sha512-RIJVx67EHTmO10t0+v5qbV9ifV7OWtCZqXqQfQWJuz7LgQ6ik80HjuaNlHWt0M5quRCWx/k6mQsKFOYyeb6L7A==, tarball: file:projects/server-collaboration-resources.tgz} + resolution: {integrity: sha512-B4NebGlGzrBLndp/oc187Z2iI1omiM7Yn63fLVSSDyw9a2CJV+UH3gbkg7Yh4AL6cEV+Kl+sRaY6GutCNvyMjw==, tarball: file:projects/server-collaboration-resources.tgz} version: 0.0.0 '@rush-temp/server-collaboration@file:projects/server-collaboration.tgz': - resolution: {integrity: sha512-hRLLhddqXz9FapPNDXL2midAJcZXVhWynb+OPTcGYO5ZhMoj7ACDQfgVFKkGknE4eqU1PDxJNTFX3uchfzyc2g==, tarball: file:projects/server-collaboration.tgz} + resolution: {integrity: sha512-rj+TYtBa3dxDzg64IDzoB9fuXnVbvqf+dBxN3rNz3YukYQMczDVrKuAxd6/B9YGMTKgS92BkzDNT67K4l+xPvg==, tarball: file:projects/server-collaboration.tgz} version: 0.0.0 '@rush-temp/server-contact-resources@file:projects/server-contact-resources.tgz': - resolution: {integrity: sha512-RwZXSySutWd2DhDWrm9lNZPCpMSM0MclSJF6tFQWDCjs7YawrHQXjhlbbW/gYNnhyq2l83zQ/NZh9vPri2/R1A==, tarball: file:projects/server-contact-resources.tgz} + resolution: {integrity: sha512-WIt8A0ycqrTvOqSsZlbnX0WPtZmGOG1uS62yXoKn4lEhagNHRQtr28Vw+UeipqvNBR2rIhQc6nZet9OG8CbGmw==, tarball: file:projects/server-contact-resources.tgz} version: 0.0.0 '@rush-temp/server-contact@file:projects/server-contact.tgz': - resolution: {integrity: sha512-N+t3kpGAEHEUmlJkFHIMjZHAWUQWoAwHL1IPT/Hob3D/+a26jjym4Zswxq1PzIpd/1aqhUqfmGE7jQ/0yrPknw==, tarball: file:projects/server-contact.tgz} + resolution: {integrity: sha512-fOHoPQ4IfLjBxCSzQwR0jv1j+rB+9aZVA4elhMHLLtufEsMDyyd/KO4wrSQXO0xzGlSB5osMv4ORurn7qjjjOQ==, tarball: file:projects/server-contact.tgz} version: 0.0.0 '@rush-temp/server-controlled-documents-resources@file:projects/server-controlled-documents-resources.tgz': - resolution: {integrity: sha512-+6rWeFmVL1qv3nvO7L/Tudufb3+lvVsKG4n/TiCECBbTuXkThkcFYGGEHVLsYXedhVOzFyKtq9EADFM9lquYCg==, tarball: file:projects/server-controlled-documents-resources.tgz} + resolution: {integrity: sha512-8SDfSLWZWqoPq/gNegwbniaLhBUnmXHyyCCxG+wuXoPDpnohRcHqtQKQBM2clarZhEPuAbx92DlzEGW38Ped3w==, tarball: file:projects/server-controlled-documents-resources.tgz} version: 0.0.0 '@rush-temp/server-controlled-documents@file:projects/server-controlled-documents.tgz': - resolution: {integrity: sha512-MIy36fut0OcwmY+iW3x5sdduhTYYSqnAvMCpxHvpPgLXpZUnB0l0jMUdnjlYtsUJIYAQkYf5DDYK1nkZgByzKg==, tarball: file:projects/server-controlled-documents.tgz} - version: 0.0.0 - - '@rush-temp/server-core@file:projects/server-core.tgz': - resolution: {integrity: sha512-mqZvbYfABjM2XSOfjIPaih4jgStL/juNx5KiKzCmtT++fX2JsXvXCbb3D2ycvfoCxtPqI/+g4XWHfXk0YZd9Aw==, tarball: file:projects/server-core.tgz} + resolution: {integrity: sha512-23rHqHQPHp7Mp+SoMqZhm3iK0E+xU5F41vfH8NQJ+4YuHYHPoeKoeWzvA7Bbq8tF1ti3z/rYGHZHDHfGXf0N3Q==, tarball: file:projects/server-controlled-documents.tgz} version: 0.0.0 '@rush-temp/server-document-resources@file:projects/server-document-resources.tgz': - resolution: {integrity: sha512-oGeNjUO9nhLe1Pn6ZgkrxRqhf2VHrv8yInR/Q++cVCrn4U4077vHCB9IfdVrgjdZgoin6yEgkz60Na/KfVWBCA==, tarball: file:projects/server-document-resources.tgz} + resolution: {integrity: sha512-XAz0pcq6v8x0KilHGK2aC4Gy5dTFz3v/YWKWRlmBh5sFfgoQhe2RVi9SX8D5rEixz7Mz3Y8bhM+4pGKbHG0NEQ==, tarball: file:projects/server-document-resources.tgz} version: 0.0.0 '@rush-temp/server-document@file:projects/server-document.tgz': - resolution: {integrity: sha512-mSj++O7XQWyOoprxiTu4C4GQbmZdEFphVOSE4qlQb13+WP2klq4udw7wvv5gZQMvUHVIndpavDim3JONzuvpRg==, tarball: file:projects/server-document.tgz} + resolution: {integrity: sha512-nvq/fAWXhAIZ3hFC5nHZdP2PMnl7K235c44h0MI9cWtTgf2CJCDeVgGzFwd7AD00tgLS2waHYObBrSvYFEuaYw==, tarball: file:projects/server-document.tgz} version: 0.0.0 '@rush-temp/server-drive-resources@file:projects/server-drive-resources.tgz': - resolution: {integrity: sha512-UPPMRxy06JMA/XEiF7OEPcVKjdP5uGAdlX3xaT70VKVR1UMMyIgIxd+SPG2PQFgyHJpe/u4x+aRhGw7dSZtCiA==, tarball: file:projects/server-drive-resources.tgz} + resolution: {integrity: sha512-p+hkYRUSsfGG+wn7TpRgCMwShgFYcdCTld/1+gPW2bJF1EGqmSRs3pBj5wE5ydvM+Ol7Lb6M8a/e43MTenqPIw==, tarball: file:projects/server-drive-resources.tgz} version: 0.0.0 '@rush-temp/server-drive@file:projects/server-drive.tgz': - resolution: {integrity: sha512-hU1fz5qEpucByHn2WbQ+TmdOAuuI5doOe/yKxfOiTFzm91iwocZIM4yEphcZiZbZHmZKcnCHjZ5+z1xnwlzJEg==, tarball: file:projects/server-drive.tgz} + resolution: {integrity: sha512-Z7zuLFFdslZXtXwR5gWOejLztqIXlSnLujDxc1jaqQhqdsLzCJ7OJV7KQ6sSkwZhOWRR9ppSmQigmJBKIp+EeA==, tarball: file:projects/server-drive.tgz} version: 0.0.0 '@rush-temp/server-github-model@file:projects/server-github-model.tgz': - resolution: {integrity: sha512-yINwt6SbVKRd1NfE6sgJyv5z/OsMFIWNSYCHu5PsanAiClT/99Wz+yazNXD/gCrgmI3Vr0IIx7KMBqCEDNg3IQ==, tarball: file:projects/server-github-model.tgz} + resolution: {integrity: sha512-dOoJaKmHZOGFstx2JSayQxcovZ+cbcTpZgzeRj409NHCaUdD7UxG2DiTttKoRaAS2sgeebPKbBxkyJWe9Gk/2w==, tarball: file:projects/server-github-model.tgz} version: 0.0.0 '@rush-temp/server-github-resources@file:projects/server-github-resources.tgz': - resolution: {integrity: sha512-31/Fn5MqhBVTRXhvfd/rPYE3zrNHPo/W1RqoyA3oGXf1j/hnzWvOrfUr8HhGlu6Nez8+HcCpX9HCAt29SVyl9w==, tarball: file:projects/server-github-resources.tgz} + resolution: {integrity: sha512-sqCrGn87gLy+ldlUwQecvAdvy2877v5HYIxdebNxKs+n1R52r6HuWok+vSS/q3YHEUCu1/ydz4odMJtHaivDIw==, tarball: file:projects/server-github-resources.tgz} version: 0.0.0 '@rush-temp/server-github@file:projects/server-github.tgz': - resolution: {integrity: sha512-t45Cn6xpQd3ZPpCX4F4lx36ocxyeHbj/vqfoDfcClsfgPAMx1+WOl04Yeyj/MpWZcyD471MGUaP8Owo+0EdB6w==, tarball: file:projects/server-github.tgz} + resolution: {integrity: sha512-EJB62xkKO9BhAS1TbkbxtRx0JcKCa1t3oj5ZG1cT5BVIKLpaaiF5hvVIF8pz8LldaJcPc5hkGPhYdvUagg6tYg==, tarball: file:projects/server-github.tgz} version: 0.0.0 '@rush-temp/server-gmail-resources@file:projects/server-gmail-resources.tgz': - resolution: {integrity: sha512-X9Hky+xdHqg7nMp/MPSgGoFaN3RApwzWF9CITD3sfllp+C35J5PVVZen8KhC4sY6rV3kQwiiVgUKnV3BURBY5w==, tarball: file:projects/server-gmail-resources.tgz} + resolution: {integrity: sha512-Adz7Nb3H0JoUpGHi1IRidk5RUX1Fa1rHXWPVkUDaYu/elKMpldA0mpGmq40NG7AuyMJQSORHuSqTkdTCPZaAqg==, tarball: file:projects/server-gmail-resources.tgz} version: 0.0.0 '@rush-temp/server-gmail@file:projects/server-gmail.tgz': - resolution: {integrity: sha512-jxQwIBVT6rq1GZ8zY+KTtM5XzWpxDuBU7nTxGT1fmi8hMOO8MfvSf62+lBHvD0hfzDqzSV+NIQBC21Il7GaNyQ==, tarball: file:projects/server-gmail.tgz} + resolution: {integrity: sha512-7TwiHM9ryHRIP/A8pXgkCunfUc6XnNOHNajgNf1vIW8jI9iS1fDRd/drpELdIQ5S2hdYY1MeU/aaTFkHzFKwCQ==, tarball: file:projects/server-gmail.tgz} version: 0.0.0 '@rush-temp/server-guest-resources@file:projects/server-guest-resources.tgz': - resolution: {integrity: sha512-kNWLAQNjP8gcDFLNjLKMcCUNIe0Qqy3wjbMfvnVJox70lNSbccVMFlJwJpw20iDGOyUqSlN/PupNIwv+5joTmg==, tarball: file:projects/server-guest-resources.tgz} + resolution: {integrity: sha512-qawVecuT0QQ0KHIVB6cLiJ/W8QvVgn1kwM156wexJR51XoAqWGiIIaAF0BWMmwBPKMC3uuKnRjK4fBWJDWInTw==, tarball: file:projects/server-guest-resources.tgz} version: 0.0.0 '@rush-temp/server-guest@file:projects/server-guest.tgz': - resolution: {integrity: sha512-uVPcRSKmVK9uOkpF0rQnZCcYVsOlwXDIP56YvofHSPWorD8Eun3q5ltvLdYJhdaNeugPBgh5oVUOlL77kE4O5w==, tarball: file:projects/server-guest.tgz} + resolution: {integrity: sha512-yGqTQEwMtjquniOeqYFt0GcbMEGW9kYgHJVbP+7gdcT7NYn1W7QhfAMzlV19dcimh57xnye/WPdhbOpUWYuvRg==, tarball: file:projects/server-guest.tgz} version: 0.0.0 '@rush-temp/server-hr-resources@file:projects/server-hr-resources.tgz': - resolution: {integrity: sha512-FhVFQwY7s+anSPCbg5LuYWBkWZQ743WHYteGIbu5isa4C8TcFKPazr0M8Pfh4ZihqA6eOdPDYFMaIZcBDbgpdA==, tarball: file:projects/server-hr-resources.tgz} + resolution: {integrity: sha512-KpeTdiWuq/y9Md2ladX7aiOuFPD7ib2MG2LQkjxCBTPVJ8nBgQLUHqEUKLfB8Zr3yW7lxmhLiINXTGfrVneIrw==, tarball: file:projects/server-hr-resources.tgz} version: 0.0.0 '@rush-temp/server-hr@file:projects/server-hr.tgz': - resolution: {integrity: sha512-+gAqaM+g4i9OndD3pjErvfMcV+b3FgYaoR3BA6CUfROLB0orKcz97CU0F44yiJopaJ3Yvrx9EpLYYUrak3jTuw==, tarball: file:projects/server-hr.tgz} + resolution: {integrity: sha512-wVBJZ8XjABj6d8pC/5+KQo5YlzU9Ace0ihUkkPPEAUeCI6wBSSVXGD0YcdeSWpdTzE74rAxbzHstqpm3mkLbow==, tarball: file:projects/server-hr.tgz} version: 0.0.0 '@rush-temp/server-indexer@file:projects/server-indexer.tgz': - resolution: {integrity: sha512-XDbXs5oyc5RqdJSRLaEdPrb3f3tDgmyzdQ5XC8lNRYIda3oceA0ZBP1zLDd6nks2CZxo3qNhUmNCp89XecT6CA==, tarball: file:projects/server-indexer.tgz} + resolution: {integrity: sha512-MfhOaIWeeuaz+Mus842y7CZcs3Z3YRrCDg2pUKmgfTLhbhSPbhkCT1PSElWW4VbUmCfqH8PAdhE0bmh7h5/8BQ==, tarball: file:projects/server-indexer.tgz} version: 0.0.0 '@rush-temp/server-inventory-resources@file:projects/server-inventory-resources.tgz': - resolution: {integrity: sha512-GyOKbT8HkOZ6/DmRxHa7xaZgZSNlOCkd3yMNJyT/KHezM9F+J+0bDWtd1QQwf0SAhBEaOjpodVcmuzK3h4IsHw==, tarball: file:projects/server-inventory-resources.tgz} + resolution: {integrity: sha512-W7Ginmf/bzcqrdUnB3Eo3xb/VN6uHwT/FnKxy5HOyaPdjWHaCGaYWUN2HfgMgMyLW1btdtPH8ZZjhB4xVOqv7Q==, tarball: file:projects/server-inventory-resources.tgz} version: 0.0.0 '@rush-temp/server-inventory@file:projects/server-inventory.tgz': @@ -5747,139 +5733,131 @@ packages: version: 0.0.0 '@rush-temp/server-lead-resources@file:projects/server-lead-resources.tgz': - resolution: {integrity: sha512-getaS8ezR/sLN/Koa9AXqk8DWd+UzwfpGwfnwHmd+JF/Eju7k6ysa8/nUqMSctCTdfYCl9+UOerdABKFaKTE8g==, tarball: file:projects/server-lead-resources.tgz} + resolution: {integrity: sha512-RtU0ZjXMzAm9lUrde5wiN+IckeO1mGS42Mz/or0QHUrdGxOuLFGwvmEGANLvBgvLTmW6iMlfk61nExEPe3LEqg==, tarball: file:projects/server-lead-resources.tgz} version: 0.0.0 '@rush-temp/server-lead@file:projects/server-lead.tgz': - resolution: {integrity: sha512-Yz1skS+dxJyv2seCb8ZkV7Hvj4TlOao4AIVVMoCpobGIPr+PUUYaqAuueFABjwLYfih+r/YQVA6ZxyECV87RgA==, tarball: file:projects/server-lead.tgz} + resolution: {integrity: sha512-qmYuUsUFQLwlt/XjKFs+pPH+ywVO6OaPZc5A9oo0DBIiQVexQFlsaS3dDGHsZxFTXlB0ACmnO3sRmHSypNIJPg==, tarball: file:projects/server-lead.tgz} version: 0.0.0 '@rush-temp/server-love-resources@file:projects/server-love-resources.tgz': - resolution: {integrity: sha512-Ryg+RoI4iL8SInbP0j/Gc53rUHlDUPbRrTFbbZvfu8DIR68iPHaqZTjbkNiUZgOGQrwrgcJ+yu0DsyyLQZzBhg==, tarball: file:projects/server-love-resources.tgz} + resolution: {integrity: sha512-tzNmaNmLu56+9Usa81jH7YzpHq5iJlOvsXlJTBtIOuBcW4FHBP+WETodeGAwVUAInDGEdTRGowZ9zTit5fcyHw==, tarball: file:projects/server-love-resources.tgz} version: 0.0.0 '@rush-temp/server-love@file:projects/server-love.tgz': - resolution: {integrity: sha512-+J+jifYKRA9Bbhk5xM/jKiYkl2xVicq/5iGGkfdkWeU139FecNdp18Tiub6+ZludXB/E8xAWc0/TnylpYSdZKA==, tarball: file:projects/server-love.tgz} + resolution: {integrity: sha512-4hrw2VpsH0bQ3GtraKvBccD2S/Y7KykcJXKkENPCbsCI620JtHPwBT7lrqqEJKEmAv7MtAIW1lOalAxR2KLuGw==, tarball: file:projects/server-love.tgz} version: 0.0.0 '@rush-temp/server-notification-resources@file:projects/server-notification-resources.tgz': - resolution: {integrity: sha512-DnXtvVF8eaMbE6LA1tbkUGC7ZB6QPUsK2FzZIqs4b5AyMOZQXNrFDVseoTRBQ4V6kRP7ou7Tu5CNQYsxF0Ra8w==, tarball: file:projects/server-notification-resources.tgz} + resolution: {integrity: sha512-eb0Nci2ae++JMEN0jxkzN+pc/E7sKLT1OMEDtjwTn+IEPpjAbo4k5vVYOOjWfRz64b8lKne7ZPIjKNzDA0423Q==, tarball: file:projects/server-notification-resources.tgz} version: 0.0.0 '@rush-temp/server-notification@file:projects/server-notification.tgz': - resolution: {integrity: sha512-+3rVDNncsDwkMGt1Y0ghxttG6J1eCHTlPNDqcG/M1/oe5hb6E2f1phQAbftrB7JJQWpaXQAIYbRhoNRR2l20pQ==, tarball: file:projects/server-notification.tgz} + resolution: {integrity: sha512-BgKmv+BPeDpGhgqOYhDKHnJZ7QoREfufUXALMNI1+xbvRUco58c+/xM5/yRtzIFm+5lBGBbNNujreGJQdBAM2w==, tarball: file:projects/server-notification.tgz} version: 0.0.0 '@rush-temp/server-pipeline@file:projects/server-pipeline.tgz': - resolution: {integrity: sha512-KwqL/lyDS5P7qy3xycO/GS3Fz1TC6EYzaLkIQm0EhgPuodQAx2/yFxQDAqdLOiFMc7/orlkz2M5mF5qatrsbCA==, tarball: file:projects/server-pipeline.tgz} + resolution: {integrity: sha512-uvllffYDvFL9MOKGa7XtfYoFEnD/j1QuYeQd/4igfmS0N+3XAEyNDzYKPkiIzNMkgreFazJzfHAQqK/5RMEMrQ==, tarball: file:projects/server-pipeline.tgz} version: 0.0.0 '@rush-temp/server-preference@file:projects/server-preference.tgz': - resolution: {integrity: sha512-9MLDz7Kd7kCH5qZZg/Xh6e7Ib3xza+iWdMs/05Gg/glG6UGqP6KFvSsoc2FnM8nPWm4Mp+oUQ1Q7T8+i2pCs1w==, tarball: file:projects/server-preference.tgz} + resolution: {integrity: sha512-pfqY28GHGEtOZsk735tt7GtDR+eav6rbjMgKMaVJyPaW6O6Yvcf2DO3bT65Wc5xSs+UzXjZGe5cZ7Cgzb3W+RA==, tarball: file:projects/server-preference.tgz} version: 0.0.0 '@rush-temp/server-process-resources@file:projects/server-process-resources.tgz': - resolution: {integrity: sha512-7eVLndwMnSkpEtBsC7ZoNWOYo3fh+KuKhDdG8azA+LTyXd+1rO/uKlQsdSBO7oZNes4uxL4AlpU/wu5y/SQF6Q==, tarball: file:projects/server-process-resources.tgz} + resolution: {integrity: sha512-oCJyBhiJ/GLXANCp54TyxwOhGumE++JkWLqYLKYIVmydZP+VjdXY/OLSC5fsIKV/bA3TeH5KV9mCs/4PLPtCtw==, tarball: file:projects/server-process-resources.tgz} version: 0.0.0 '@rush-temp/server-process@file:projects/server-process.tgz': - resolution: {integrity: sha512-pFwIiHn0XrfTKDCuCHzag/FtiOYD3pOEslzKFqnVG4lzdQ6YbfrVTB5JBBFvgu16ztRVHSaWiWLaq1cT0xn8YQ==, tarball: file:projects/server-process.tgz} + resolution: {integrity: sha512-bhODTyFQ6WjNZaLVvIQlTMiKlJQgxxLl7+FTaL48bovra44p118vM5mzKUVy7S2V9RXDdn13fpQ2FhX7WWcO1w==, tarball: file:projects/server-process.tgz} version: 0.0.0 '@rush-temp/server-recruit-resources@file:projects/server-recruit-resources.tgz': - resolution: {integrity: sha512-PVnZP4Vf+dYlmaEVRVHzMyIb0zWeApeMqAoLLCGjgn59V8nHTOUVTubyMAnfoNn99bdq5xVz/zq2IKE6oVhF/w==, tarball: file:projects/server-recruit-resources.tgz} + resolution: {integrity: sha512-NCU3E0D8uhYOswLyJRLVq5Fcjf0HhRI0vidVjaCFkd9JQ8anspG6tCQhskHI/4mMBtOWmOthdP/TLd7FfxlNfQ==, tarball: file:projects/server-recruit-resources.tgz} version: 0.0.0 '@rush-temp/server-recruit@file:projects/server-recruit.tgz': - resolution: {integrity: sha512-nyhy5GW+gNPdAIjbGHshrOKls1LlNeWq2FxCQ6AJc5p7qdiNFrUg+WmVFzTZSPJSPG46fGl66up1kTPPpRjwfQ==, tarball: file:projects/server-recruit.tgz} + resolution: {integrity: sha512-/vJ9Pi1KSxNJ/s7Qg4O4e/gDXAjxujVpz+qJm9a1Au9bS5dx9dJQW2Sq3mwbG7tvRacs2TadDhutgh+ArL1A7g==, tarball: file:projects/server-recruit.tgz} version: 0.0.0 '@rush-temp/server-request-resources@file:projects/server-request-resources.tgz': - resolution: {integrity: sha512-9osbg5AUt6cQ1tgJ8QzZYC00L8LKX7UCYmBfAwwH8Kk2O/UAXzPuxoXt6VIwtFya60BznhfA7jmdHiS+nmN2JQ==, tarball: file:projects/server-request-resources.tgz} + resolution: {integrity: sha512-WFiwNptXt901F0hRg5VobdkTTMCPcw/tsJ7bkL9LY1iWkKSC68kejRzS0qSp/uljhy/Ckghg22kahKumUpuIYA==, tarball: file:projects/server-request-resources.tgz} version: 0.0.0 '@rush-temp/server-request@file:projects/server-request.tgz': - resolution: {integrity: sha512-S5vZ7pboii5mcO0Ti8qzMS541aJ4PZpL/3LXx87WRW4zjw/z8S0Uw4/jClpLVaqs86O9uul2udheNTHoKWwBWw==, tarball: file:projects/server-request.tgz} + resolution: {integrity: sha512-rbggVO8aDAV3vYjQSAxpiT7Ye2hoOIUWumQG5XXnmFcAlMG6xUtcHg1CMb2tU6MDZDsrW8SHc5lHMx2nD376cw==, tarball: file:projects/server-request.tgz} version: 0.0.0 '@rush-temp/server-setting-resources@file:projects/server-setting-resources.tgz': - resolution: {integrity: sha512-vLIvPWWGrVH84SMGjCcvzx3SFJ0UAd835zmSYZAsABqMm2wxRFgktOJ/DPmXA4VrLIFHqHg/525g20dQNnhQvw==, tarball: file:projects/server-setting-resources.tgz} + resolution: {integrity: sha512-IG6sBJaxPoLaPoAcLml8OHh5a3ZOlelR2tAHjc7g4aKeztR8hwOPxwFf+E9Y6igZ7krfU+sa0HC0kOQab1pNrg==, tarball: file:projects/server-setting-resources.tgz} version: 0.0.0 '@rush-temp/server-setting@file:projects/server-setting.tgz': - resolution: {integrity: sha512-4pI7iV5+1qe7pBF2LcfPlbCdEv1xxlvTunakVT+MP8oeItvGDDVEirRjPdUl6D0l00k0J292BiSwln+xcq1DkA==, tarball: file:projects/server-setting.tgz} - version: 0.0.0 - - '@rush-temp/server-storage@file:projects/server-storage.tgz': - resolution: {integrity: sha512-oZPCkrsG3DRnhZRKBmZnzV2jeLUReMYOvbrc7Izab0yk0lCI1ODAPm2K7fgzcjMBBXrScFNc78lPBTx9LBZUoA==, tarball: file:projects/server-storage.tgz} + resolution: {integrity: sha512-YyMdyE/TQMXqFC22BkPCJModYHvIkrH4HErBBiBBo8mIAfZVvQk5B9XA/UySFkXOvQzgYy9KAvzlRJ38svk75w==, tarball: file:projects/server-setting.tgz} version: 0.0.0 '@rush-temp/server-tags-resources@file:projects/server-tags-resources.tgz': - resolution: {integrity: sha512-dCFtBoZ2e89pqQQKZyUiHC9n95em5ZlkQ5sMk0IxXu3MZktxRAtKyJnm/kWlGK3N93rnciV64qcsXYi6HdHf2Q==, tarball: file:projects/server-tags-resources.tgz} + resolution: {integrity: sha512-cQd72eCt5bvrDxmTEbHrIi9QBhGGyOwVpGX4efYvAHdIglxPhvPjqh3ehNvtF+b+ATLMWRhpPCgGzNxdjAtvyQ==, tarball: file:projects/server-tags-resources.tgz} version: 0.0.0 '@rush-temp/server-tags@file:projects/server-tags.tgz': - resolution: {integrity: sha512-15V2O0CnGd0y9bN2+GMh+I27ta0rDTX4GS8+MPLKW9N7oes421Xl4h72piPPTh5+ZhsAJlFCuvlI+rHCdhBsYA==, tarball: file:projects/server-tags.tgz} + resolution: {integrity: sha512-oLdmbPkdOgTr5kx5620pVSbws1tx5eJzSC2vDA5QIiocWp72rNygtpEEsOVdNqJ3gBTAvyQjKlQs0ARWzux2+Q==, tarball: file:projects/server-tags.tgz} version: 0.0.0 '@rush-temp/server-task-resources@file:projects/server-task-resources.tgz': - resolution: {integrity: sha512-Yr0lW9FLiacv9xHsK+nKrZXg8xJbhtSbhn1OvFuNSdJLX6OAsefTgeXF/tc1g5pITHr63rRghYf86hPZfGdL9g==, tarball: file:projects/server-task-resources.tgz} + resolution: {integrity: sha512-pgUH8lI+47m9Q2Ktz/YRlXjL7Z13/kBMiK/4qqtW9ML9seUmTJ1iC8VB+acMIW6ktHHI7pc+3Fyi6jZCBNjz8Q==, tarball: file:projects/server-task-resources.tgz} version: 0.0.0 '@rush-temp/server-task@file:projects/server-task.tgz': - resolution: {integrity: sha512-T+YHmwMlTCacugW2p+oANy5BpLQ8Eg24q2Jys54hVvt0RR5XzRhxZlfuXk8UlaE3iLZD6PWzc8AzT57bOBBq+Q==, tarball: file:projects/server-task.tgz} + resolution: {integrity: sha512-/EPnKCSh9fRbUbnDhgy/hSLVT/5Cvn3xGI5kPIiA2jbo/hSKaSgCp7TTcnZSMSzqROi7LNowjeIZEOtPJvue2Q==, tarball: file:projects/server-task.tgz} version: 0.0.0 '@rush-temp/server-telegram-resources@file:projects/server-telegram-resources.tgz': - resolution: {integrity: sha512-vsDMwiP91QFlr7Aco90ejBpB/FJyxXDq7jho6zTcNdLhC6CeLFNeKZh5BnPFMgo3Avri9GyVBx/Jn2aIb6s8ZA==, tarball: file:projects/server-telegram-resources.tgz} + resolution: {integrity: sha512-ZhjzG8Btab77h7yBihGn/N7WsyHmFoJS72IpoxeYEH7/7lfCN5HxYBIEss7NfOt2Y+6Wj4luXfQeQ+7d+j7ItA==, tarball: file:projects/server-telegram-resources.tgz} version: 0.0.0 '@rush-temp/server-telegram@file:projects/server-telegram.tgz': - resolution: {integrity: sha512-3gzc9PdJ5rkUaS0xZskyU5oipykWwKmrZa7IMCwqoGW7k5oEjWT1LL95sl9FRvuS1uWHFl8Tp7r1Gc8Q1bYhpw==, tarball: file:projects/server-telegram.tgz} + resolution: {integrity: sha512-5c8LnlzO+Yq1h7XWop0v6ezQWlGtZNCokciqH9GR2wgRx5dus1aW1a6K7j4DDcrGzJ1k81OYg+aC68j33xw1XA==, tarball: file:projects/server-telegram.tgz} version: 0.0.0 '@rush-temp/server-templates@file:projects/server-templates.tgz': - resolution: {integrity: sha512-Q1jcDwB1MijVvC4pfsEI0RWhPNk0TzX8uujICj243f3LunRae5vk77O+q5cN0gnrdNY7x/oExAKWj7ga6lWnUw==, tarball: file:projects/server-templates.tgz} + resolution: {integrity: sha512-q14Pt2xfnzLX6YdlC+lUEcfPDAXMkT4DWcs+7kHZsosOt+enoBF6V+CtU3TmXcP1CSQKbbIXPuwg1xUHiOGf2g==, tarball: file:projects/server-templates.tgz} version: 0.0.0 '@rush-temp/server-time-resources@file:projects/server-time-resources.tgz': - resolution: {integrity: sha512-ZywesfxkiYVuPfGAR/G2Ldbtjtq9gPe0sS21L/q9JzV8BTa3VtpGjipkOkiKAsPA9mKv9PARCn+lFWkPpF51iw==, tarball: file:projects/server-time-resources.tgz} + resolution: {integrity: sha512-kqYUCIYQpPFcyO1IdKMMSqPGQgJJQeNAL1ZTwh8k2Upgr+qob/0T3/ras7dxpvGRPdZFYtmDnjdLJSFQQEreZw==, tarball: file:projects/server-time-resources.tgz} version: 0.0.0 '@rush-temp/server-time@file:projects/server-time.tgz': - resolution: {integrity: sha512-bPVb3BS5ozweLUyUiKkf2hYptv3Wrez60jXAA3t9Fx5TEG2hCC8dZM6xnp6kU/hk2OXKMTf02huk1TM3R5tBJQ==, tarball: file:projects/server-time.tgz} + resolution: {integrity: sha512-MZ5ul2UjxGQAEHscCcBB7KvTmrQ362d5l0zutCMyBT6VRiI3ei+LMlINoMQW7n7IwsF7NrZYY4oIxru7zZY6Ew==, tarball: file:projects/server-time.tgz} version: 0.0.0 '@rush-temp/server-tool@file:projects/server-tool.tgz': - resolution: {integrity: sha512-AhZOcDsajDCwlhb3m0lSQFeq4n/ZIngqapeNTdoRwZLlSTc4VV/UAoPHGMScvjykAgNUDVjv3GyvLFqEZ1Swyg==, tarball: file:projects/server-tool.tgz} + resolution: {integrity: sha512-qNB+2uHAM98kvgSy87afWBNUfp6xDZ3j7u33eeiJivySIqEmxl37YnnUsz4s7BLStM2997S1TVjkFSwqwn2RiQ==, tarball: file:projects/server-tool.tgz} version: 0.0.0 '@rush-temp/server-tracker-resources@file:projects/server-tracker-resources.tgz': - resolution: {integrity: sha512-NT9mNvWeGmGgx5C9y2CfptF6glG4/Eo340tzalG9NYy7p9pYdZTDmvWyayPTBwPXyMbufaZxt3lewA9Ie6Y7CA==, tarball: file:projects/server-tracker-resources.tgz} + resolution: {integrity: sha512-yVmju0Vx4ij5+LwLm+mM/u4muhMTt7DYS+XOX2qhNfcrWGqXhCsbnUNAx2HZHszqSlAUQYDMMEUzdi/eo+wGDg==, tarball: file:projects/server-tracker-resources.tgz} version: 0.0.0 '@rush-temp/server-tracker@file:projects/server-tracker.tgz': - resolution: {integrity: sha512-0w0ogcC1SC3oMpaPA7mtMVsZj2IeTxGJpf/ZMM48APtLIsDcRb5LEuCoNhkHZSE0athBgRt5B2D1sB8Sg2I6rA==, tarball: file:projects/server-tracker.tgz} + resolution: {integrity: sha512-BDFU9yOxl1cvTfW84AlqnRsmG78QIBuQ9yveJO5ZdCprJBztYaYX5BKGu7UpDAN1Ss6PKNHcZqZWmMR8RFLYRg==, tarball: file:projects/server-tracker.tgz} version: 0.0.0 '@rush-temp/server-training-resources@file:projects/server-training-resources.tgz': - resolution: {integrity: sha512-JsGwG5v2MIHKjyZ0UM+G9G8jUMTuTsyL70GFIs7fq4wNGVDJytgtMyFT5IgIPK6ykvqbsSK6NivJ4nATcuwBeQ==, tarball: file:projects/server-training-resources.tgz} + resolution: {integrity: sha512-xykXCzcRAkbASTBp8w0veqh2CaGx/5+500xYZoFUkrEZBJ8SvxBU8wHexH8O0rYQWJv/drqUqLKZWPygPBRvTQ==, tarball: file:projects/server-training-resources.tgz} version: 0.0.0 '@rush-temp/server-training@file:projects/server-training.tgz': - resolution: {integrity: sha512-Sy7F1KuRNdfH+of9mCzMhayR0zqmj8m04GPHZXxRC/kB/N5PIUYURJoVF3aRgM3Jf8TXu2atzwNGHipXg7q18g==, tarball: file:projects/server-training.tgz} + resolution: {integrity: sha512-vQdxW84CCQr/DXtSBApjQ7gt0CjxaK26D9dTVgfAACC/Dtvem3WuNWZ1wrZhrXLh8ANEBXLegEkl7t9pSAy0lA==, tarball: file:projects/server-training.tgz} version: 0.0.0 '@rush-temp/server-view-resources@file:projects/server-view-resources.tgz': - resolution: {integrity: sha512-eOiuM6gW70rTPym2bjG7da/nxX6FLLhkwNilX971cFI8g+AzdbGlwOJROXP9B8OHl50psQmypNbcePSvYWVVLw==, tarball: file:projects/server-view-resources.tgz} + resolution: {integrity: sha512-Fl37N1MlOuJGvZjQaC8K4UqHiw1aLcloOKAinRqPHAzP5uULFmYW1vAd0jIGLwq11y/otuJmRIKgRf5gQWBfDA==, tarball: file:projects/server-view-resources.tgz} version: 0.0.0 '@rush-temp/server-view@file:projects/server-view.tgz': - resolution: {integrity: sha512-LHWF5upJuS4Q5/5C+6hqRiaHgrFygsfq3bbQQRdB6nDHVMqbPKRDakfz+8CHOw+FqdoI5K/HcXZw+ZWVDGU0Zg==, tarball: file:projects/server-view.tgz} - version: 0.0.0 - - '@rush-temp/server@file:projects/server.tgz': - resolution: {integrity: sha512-a7bIGolnxBglCj5/NjB9d5SktHyCkqvwJwBsNzOx8pesLQITbwCtRWtBnAMmHN+NrsDlRyz5dNDfg3C6Lf8Ohg==, tarball: file:projects/server.tgz} + resolution: {integrity: sha512-r2WTozGDP6SR1SnJectvLLFaygB6cWy8mcAeU5dRbNmc65JatsRnl9vN9tqsrN+BMR2aBnsHH06r/93OWlzBeA==, tarball: file:projects/server-view.tgz} version: 0.0.0 '@rush-temp/setting-assets@file:projects/setting-assets.tgz': @@ -6019,7 +5997,7 @@ packages: version: 0.0.0 '@rush-temp/tool@file:projects/tool.tgz': - resolution: {integrity: sha512-hRpgWKIRkJsIATb/no75l+M02oBljamtpc66vF33kdqTMSNiBvKwCcEylOa3o9PmMJMY52KNTW4omescgkQUhw==, tarball: file:projects/tool.tgz} + resolution: {integrity: sha512-I8ZlYeK3rReOAH9E5XPnfwLUCI8K6sGiEVXxvPm8OrtZNiet2UUAMNcj+l/FnEMsp5uxTEZ9mEM/2+Qkc1YO3Q==, tarball: file:projects/tool.tgz} version: 0.0.0 '@rush-temp/tracker-assets@file:projects/tracker-assets.tgz': @@ -6087,7 +6065,7 @@ packages: version: 0.0.0 '@rush-temp/workspace-service@file:projects/workspace-service.tgz': - resolution: {integrity: sha512-y9gDIyaSj902fFE7VXUlD2MHIQAkumQ6bXQFKR+63BhCNIor9kOp1TO38rYebc7e2cI5fgTnR0LV1xbToTzS3A==, tarball: file:projects/workspace-service.tgz} + resolution: {integrity: sha512-Dpg3WjYXFQ0LRDOdtoKbZF47/jjDgS/E+ADmP2pj0wjiTAmGVYUVPO9wChQtlmn0o/NPWSdeUANgQBzP1Va4QQ==, tarball: file:projects/workspace-service.tgz} version: 0.0.0 '@selderee/plugin-htmlparser2@0.11.0': @@ -15635,6 +15613,20 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 + '@hcengineering/collaboration@0.7.0(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)': + dependencies: + '@hcengineering/core': 0.7.3 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) + '@hcengineering/text-ydoc': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) + base64-js: 1.5.1 + yjs: 13.6.27 + transitivePeerDependencies: + - prosemirror-inputrules + - prosemirror-model + - prosemirror-state + - prosemirror-view + '@hcengineering/collaborator-client@0.7.3': dependencies: '@hcengineering/core': 0.7.3 @@ -15710,11 +15702,36 @@ snapshots: '@hcengineering/platform': 0.7.3 fast-equals: 5.2.2 + '@hcengineering/datalake@0.7.0': + dependencies: + '@hcengineering/core': 0.7.3 + '@hcengineering/platform': 0.7.3 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-token': 0.7.2 + + '@hcengineering/elastic@0.7.0': + dependencies: + '@elastic/elasticsearch': 7.17.14 + '@hcengineering/analytics': 0.7.3 + '@hcengineering/core': 0.7.3 + '@hcengineering/platform': 0.7.3 + '@hcengineering/server-core': 0.7.0 + transitivePeerDependencies: + - supports-color + '@hcengineering/hulylake-client@0.7.3': dependencies: '@hcengineering/core': 0.7.3 '@hcengineering/retry': 0.7.3 + '@hcengineering/kafka@0.7.0': + dependencies: + '@hcengineering/core': 0.7.3 + '@hcengineering/platform': 0.7.3 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/storage': 0.7.3 + kafkajs: 2.2.4 + '@hcengineering/measurements-otlp@0.7.10(encoding@0.1.13)': dependencies: '@hcengineering/measurements': 0.7.10 @@ -15738,6 +15755,22 @@ snapshots: '@hcengineering/measurements@0.7.10': {} + '@hcengineering/middleware@0.7.1': + dependencies: + '@hcengineering/analytics': 0.7.3 + '@hcengineering/core': 0.7.3 + '@hcengineering/platform': 0.7.3 + '@hcengineering/query': 0.7.3 + '@hcengineering/server-core': 0.7.0 + fast-equals: 5.2.2 + + '@hcengineering/minio@0.7.0': + dependencies: + '@hcengineering/core': 0.7.3 + '@hcengineering/platform': 0.7.3 + '@hcengineering/server-core': 0.7.0 + minio: 8.0.5 + '@hcengineering/model@0.7.3': dependencies: '@hcengineering/account-client': 0.7.3 @@ -15749,6 +15782,22 @@ snapshots: fast-equals: 5.2.2 toposort: 2.0.2 + '@hcengineering/mongo@0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)': + dependencies: + '@hcengineering/core': 0.7.3 + '@hcengineering/platform': 0.7.3 + '@hcengineering/server-core': 0.7.0 + bson: 6.10.3 + mongodb: 6.16.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + '@hcengineering/platform-rig@0.7.10': dependencies: '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -15773,6 +15822,14 @@ snapshots: dependencies: postgres: 3.4.7 + '@hcengineering/postgres@0.7.0': + dependencies: + '@hcengineering/core': 0.7.3 + '@hcengineering/platform': 0.7.3 + '@hcengineering/postgres-base': 0.7.6 + '@hcengineering/server-core': 0.7.0 + postgres: 3.4.7 + '@hcengineering/query@0.7.3': dependencies: '@hcengineering/analytics': 0.7.3 @@ -15793,6 +15850,61 @@ snapshots: '@hcengineering/platform': 0.7.3 msgpackr: 1.11.2 + '@hcengineering/s3@0.7.0': + dependencies: + '@aws-sdk/client-s3': 3.738.0 + '@aws-sdk/lib-storage': 3.738.0(@aws-sdk/client-s3@3.738.0) + '@aws-sdk/s3-request-presigner': 3.738.0 + '@hcengineering/core': 0.7.3 + '@hcengineering/platform': 0.7.3 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/storage': 0.7.3 + '@smithy/node-http-handler': 4.0.2 + transitivePeerDependencies: + - aws-crt + + '@hcengineering/server-client@0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)': + dependencies: + '@hcengineering/account-client': 0.7.3 + '@hcengineering/client': 0.7.3 + '@hcengineering/client-resources': 0.7.3 + '@hcengineering/core': 0.7.3 + '@hcengineering/platform': 0.7.3 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-token': 0.7.2 + ws: 8.18.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@hcengineering/server-core@0.7.0': + dependencies: + '@hcengineering/analytics': 0.7.3 + '@hcengineering/communication-sdk-types': 0.7.0 + '@hcengineering/communication-types': 0.7.0 + '@hcengineering/core': 0.7.3 + '@hcengineering/platform': 0.7.3 + '@hcengineering/query': 0.7.3 + '@hcengineering/rpc': 0.7.3 + '@hcengineering/server-token': 0.7.2 + '@hcengineering/storage': 0.7.3 + fast-equals: 5.2.2 + uuid: 8.3.2 + + '@hcengineering/server-storage@0.7.0': + dependencies: + '@hcengineering/analytics': 0.7.3 + '@hcengineering/core': 0.7.3 + '@hcengineering/datalake': 0.7.0 + '@hcengineering/minio': 0.7.0 + '@hcengineering/platform': 0.7.3 + '@hcengineering/s3': 0.7.0 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-token': 0.7.2 + '@hcengineering/storage': 0.7.3 + transitivePeerDependencies: + - aws-crt + '@hcengineering/server-token@0.7.2': dependencies: '@hcengineering/core': 0.7.3 @@ -15800,6 +15912,17 @@ snapshots: jwt-simple: 0.5.6 uuid: 8.3.2 + '@hcengineering/server@0.7.0': + dependencies: + '@hcengineering/account-client': 0.7.3 + '@hcengineering/analytics': 0.7.3 + '@hcengineering/core': 0.7.3 + '@hcengineering/platform': 0.7.3 + '@hcengineering/rpc': 0.7.3 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-token': 0.7.2 + utf-8-validate: 6.0.4 + '@hcengineering/storage@0.7.3': dependencies: '@hcengineering/core': 0.7.3 @@ -17616,6 +17739,7 @@ snapshots: '@hcengineering/analytics': 0.7.3 '@hcengineering/core': 0.7.3 '@hcengineering/model': 0.7.3 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 '@hcengineering/server-token': 0.7.2 @@ -17667,8 +17791,12 @@ snapshots: dependencies: '@hcengineering/analytics': 0.7.3 '@hcengineering/core': 0.7.3 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/postgres': 0.7.0 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/jest': 29.5.12 '@types/node': 22.15.29 @@ -17693,6 +17821,7 @@ snapshots: - '@babel/core' - '@jest/types' - '@mongodb-js/zstd' + - aws-crt - babel-jest - babel-plugin-macros - esbuild @@ -18058,6 +18187,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -18429,6 +18559,8 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@tsconfig/node16': 1.0.4 '@types/cors': 2.8.17 @@ -18463,20 +18595,25 @@ snapshots: - '@jest/types' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - encoding - node-notifier - supports-color - '@rush-temp/backup-service@file:projects/backup-service.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': + '@rush-temp/backup-service@file:projects/backup-service.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4)': dependencies: '@hcengineering/client': 0.7.3 '@hcengineering/client-resources': 0.7.3 '@hcengineering/core': 0.7.3 + '@hcengineering/minio': 0.7.0 '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/jest': 29.5.12 '@types/node': 22.15.29 @@ -18496,12 +18633,15 @@ snapshots: transitivePeerDependencies: - '@babel/core' - '@jest/types' + - aws-crt - babel-jest - babel-plugin-macros + - bufferutil - esbuild - node-notifier - supports-color - ts-node + - utf-8-validate '@rush-temp/billing-assets@file:projects/billing-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: @@ -19269,51 +19409,22 @@ snapshots: - supports-color - ts-node - '@rush-temp/collaboration@file:projects/collaboration.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': - dependencies: - '@hcengineering/core': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) - '@hcengineering/text-ydoc': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - base64-js: 1.5.1 - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - typescript: 5.8.3 - yjs: 13.6.27 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - babel-jest - - babel-plugin-macros - - esbuild - - node-notifier - - prosemirror-inputrules - - prosemirror-model - - prosemirror-state - - prosemirror-view - - supports-color - - ts-node - '@rush-temp/collaborator@file:projects/collaborator.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(@tiptap/pm@2.11.7)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(y-protocols@1.0.6(yjs@13.6.27))': dependencies: '@hcengineering/account-client': 0.7.3 '@hcengineering/analytics': 0.7.3 '@hcengineering/client': 0.7.3 '@hcengineering/client-resources': 0.7.3 + '@hcengineering/collaboration': 0.7.0(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/collaborator-client': 0.7.3 '@hcengineering/core': 0.7.3 + '@hcengineering/minio': 0.7.0 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/text-ydoc': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) @@ -19356,6 +19467,7 @@ snapshots: - '@swc/core' - '@swc/wasm' - '@tiptap/pm' + - aws-crt - babel-jest - babel-plugin-macros - bufferutil @@ -19709,37 +19821,6 @@ snapshots: - supports-color - ts-node - '@rush-temp/datalake@file:projects/datalake.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)': - dependencies: - '@hcengineering/core': 0.7.3 - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@hcengineering/server-token': 0.7.2 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - '@swc/core' - - '@swc/wasm' - - babel-jest - - babel-plugin-macros - - esbuild - - node-notifier - - supports-color - '@rush-temp/desktop-1@file:projects/desktop-1.tgz(webpack@5.97.1)': dependencies: '@electron/notarize': 2.3.2 @@ -20427,44 +20508,12 @@ snapshots: - supports-color - ts-node - '@rush-temp/elastic@file:projects/elastic.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)': + '@rush-temp/emoji-assets@file:projects/emoji-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: - '@elastic/elasticsearch': 7.17.14 - '@hcengineering/analytics': 0.7.3 - '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 '@types/jest': 29.5.12 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - '@swc/core' - - '@swc/wasm' - - '@types/node' - - babel-jest - - babel-plugin-macros - - esbuild - - node-notifier - - supports-color - - '@rush-temp/emoji-assets@file:projects/emoji-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': - dependencies: - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 + '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) eslint: 8.56.0 @@ -20662,13 +20711,17 @@ snapshots: - svelte - ts-node - '@rush-temp/front@file:projects/front.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)': + '@rush-temp/front@file:projects/front.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)': dependencies: '@hcengineering/account-client': 0.7.3 '@hcengineering/analytics': 0.7.3 '@hcengineering/core': 0.7.3 + '@hcengineering/minio': 0.7.0 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@hcengineering/storage': 0.7.3 '@types/body-parser': 1.19.5 @@ -20701,14 +20754,22 @@ snapshots: typescript: 5.8.3 uuid: 8.3.2 transitivePeerDependencies: + - '@aws-sdk/credential-providers' - '@babel/core' - '@jest/types' + - '@mongodb-js/zstd' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - esbuild + - gcp-metadata + - kerberos + - mongodb-client-encryption - node-notifier + - snappy + - socks - supports-color '@rush-temp/github-assets@file:projects/github-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': @@ -21422,11 +21483,12 @@ snapshots: - supports-color - ts-node - '@rush-temp/import-tool@file:projects/import-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))': + '@rush-temp/import-tool@file:projects/import-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(utf-8-validate@6.0.4)': dependencies: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@types/jest': 29.5.12 '@types/js-yaml': 4.0.9 '@types/node': 22.15.29 @@ -21454,15 +21516,19 @@ snapshots: - '@swc/wasm' - babel-jest - babel-plugin-macros + - bufferutil - node-notifier - supports-color + - utf-8-validate '@rush-temp/importer@file:projects/importer.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: + '@hcengineering/collaboration': 0.7.0(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 '@hcengineering/rank': 0.7.3 + '@hcengineering/server-core': 0.7.0 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/text-markdown': 0.7.3 '@types/jest': 29.5.12 @@ -21633,36 +21699,6 @@ snapshots: - supports-color - ts-node - '@rush-temp/kafka@file:projects/kafka.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': - dependencies: - '@hcengineering/core': 0.7.3 - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@hcengineering/storage': 0.7.3 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - kafkajs: 2.2.4 - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - babel-jest - - babel-plugin-macros - - esbuild - - node-notifier - - supports-color - - ts-node - '@rush-temp/kanban@file:projects/kanban.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@hcengineering/core': 0.7.3 @@ -22092,7 +22128,10 @@ snapshots: '@hcengineering/communication-shared': 0.7.0 '@hcengineering/communication-types': 0.7.0 '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@tsconfig/node16': 1.0.4 '@types/express': 4.17.21 '@types/jest': 29.5.12 @@ -22127,6 +22166,7 @@ snapshots: - '@jest/types' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - bufferutil @@ -22270,66 +22310,6 @@ snapshots: - supports-color - ts-node - '@rush-temp/middleware@file:projects/middleware.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': - dependencies: - '@hcengineering/analytics': 0.7.3 - '@hcengineering/core': 0.7.3 - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@hcengineering/query': 0.7.3 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - fast-equals: 5.2.2 - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - babel-jest - - babel-plugin-macros - - esbuild - - node-notifier - - supports-color - - ts-node - - '@rush-temp/minio@file:projects/minio.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': - dependencies: - '@hcengineering/core': 0.7.3 - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - minio: 8.0.5 - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - babel-jest - - babel-plugin-macros - - esbuild - - node-notifier - - supports-color - - ts-node - '@rush-temp/model-achievement@file:projects/model-achievement.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@hcengineering/core': 0.7.3 @@ -22803,8 +22783,9 @@ snapshots: - supports-color - ts-node - '@rush-temp/model-controlled-documents@file:projects/model-controlled-documents.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': + '@rush-temp/model-controlled-documents@file:projects/model-controlled-documents.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: + '@hcengineering/collaboration': 0.7.0(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/core': 0.7.3 '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 @@ -22831,11 +22812,16 @@ snapshots: - babel-plugin-macros - esbuild - node-notifier + - prosemirror-inputrules + - prosemirror-model + - prosemirror-state + - prosemirror-view - supports-color - ts-node '@rush-temp/model-core@file:projects/model-core.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: + '@hcengineering/collaboration': 0.7.0(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/core': 0.7.3 '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 @@ -22926,8 +22912,9 @@ snapshots: - supports-color - ts-node - '@rush-temp/model-document@file:projects/model-document.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': + '@rush-temp/model-document@file:projects/model-document.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: + '@hcengineering/collaboration': 0.7.0(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/core': 0.7.3 '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 @@ -22953,6 +22940,10 @@ snapshots: - babel-plugin-macros - esbuild - node-notifier + - prosemirror-inputrules + - prosemirror-model + - prosemirror-state + - prosemirror-view - supports-color - ts-node @@ -23668,6 +23659,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -23697,6 +23689,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -23726,6 +23719,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -23755,6 +23749,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -23784,6 +23779,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -23813,6 +23809,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -23842,6 +23839,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -23871,6 +23869,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -23900,6 +23899,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -23930,6 +23930,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -23959,6 +23960,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -23989,6 +23991,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24018,6 +24021,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24047,6 +24051,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24076,6 +24081,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24105,6 +24111,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24134,6 +24141,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24163,6 +24171,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24192,6 +24201,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24221,6 +24231,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24251,6 +24262,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24281,6 +24293,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24310,6 +24323,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24339,6 +24353,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24368,6 +24383,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24397,6 +24413,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24426,6 +24443,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24455,6 +24473,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24484,6 +24503,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24512,6 +24532,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/model': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24541,6 +24562,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -24571,6 +24593,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -25035,43 +25058,6 @@ snapshots: - supports-color - ts-node - '@rush-temp/mongo@file:projects/mongo.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': - dependencies: - '@hcengineering/core': 0.7.3 - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - bson: 6.10.3 - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - mongodb: 6.16.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@babel/core' - - '@jest/types' - - '@mongodb-js/zstd' - - babel-jest - - babel-plugin-macros - - esbuild - - gcp-metadata - - kerberos - - mongodb-client-encryption - - node-notifier - - snappy - - socks - - supports-color - - ts-node - '@rush-temp/notification-assets@file:projects/notification-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@hcengineering/platform': 0.7.3 @@ -25363,6 +25349,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@koa/cors': 5.0.0 '@types/jest': 29.5.12 @@ -25414,9 +25401,13 @@ snapshots: '@hcengineering/client': 0.7.3 '@hcengineering/client-resources': 0.7.3 '@hcengineering/core': 0.7.3 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 '@hcengineering/rank': 0.7.3 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/text-html': 0.7.3 @@ -25459,6 +25450,7 @@ snapshots: - '@mongodb-js/zstd' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - bufferutil @@ -25477,13 +25469,15 @@ snapshots: - utf-8-validate - zod - '@rush-temp/pod-analytics-collector@file:projects/pod-analytics-collector.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)': + '@rush-temp/pod-analytics-collector@file:projects/pod-analytics-collector.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)': dependencies: '@hcengineering/analytics': 0.7.3 '@hcengineering/analytics-service': 0.7.3(encoding@0.1.13) '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@tsconfig/node16': 1.0.4 '@types/cors': 2.8.17 @@ -25515,19 +25509,24 @@ snapshots: - '@swc/wasm' - babel-jest - babel-plugin-macros + - bufferutil - encoding - node-notifier - supports-color + - utf-8-validate - '@rush-temp/pod-backup@file:projects/pod-backup.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)': + '@rush-temp/pod-backup@file:projects/pod-backup.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)': dependencies: '@hcengineering/analytics': 0.7.3 '@hcengineering/analytics-service': 0.7.3(encoding@0.1.13) '@hcengineering/client': 0.7.3 '@hcengineering/client-resources': 0.7.3 '@hcengineering/core': 0.7.3 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/postgres': 0.7.0 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/jest': 29.5.12 '@types/node': 22.15.29 @@ -25547,24 +25546,35 @@ snapshots: ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: + - '@aws-sdk/credential-providers' - '@babel/core' - '@jest/types' + - '@mongodb-js/zstd' - '@swc/core' - '@swc/wasm' - babel-jest - babel-plugin-macros - encoding + - gcp-metadata + - kerberos + - mongodb-client-encryption - node-notifier + - snappy + - socks - supports-color - '@rush-temp/pod-billing@file:projects/pod-billing.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)': + '@rush-temp/pod-billing@file:projects/pod-billing.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)': dependencies: '@hcengineering/account-client': 0.7.3 '@hcengineering/analytics': 0.7.3 '@hcengineering/analytics-service': 0.7.3(encoding@0.1.13) '@hcengineering/core': 0.7.3 + '@hcengineering/datalake': 0.7.0 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@tsconfig/node16': 1.0.4 '@types/cors': 2.8.17 @@ -25601,11 +25611,14 @@ snapshots: - '@jest/types' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros + - bufferutil - encoding - node-notifier - supports-color + - utf-8-validate '@rush-temp/pod-calendar-mailer@file:projects/pod-calendar-mailer.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(utf-8-validate@6.0.4)': dependencies: @@ -25614,8 +25627,10 @@ snapshots: '@hcengineering/analytics-service': 0.7.3(encoding@0.1.13) '@hcengineering/api-client': 0.7.3(bufferutil@4.0.8)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(utf-8-validate@6.0.4) '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@tsconfig/node16': 1.0.4 '@types/jest': 29.5.12 @@ -25662,6 +25677,8 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@tsconfig/node16': 1.0.4 @@ -25717,6 +25734,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -25744,7 +25762,7 @@ snapshots: - node-notifier - supports-color - '@rush-temp/pod-datalake@file:projects/pod-datalake.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)': + '@rush-temp/pod-datalake@file:projects/pod-datalake.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)': dependencies: '@aws-sdk/client-s3': 3.738.0 '@aws-sdk/lib-storage': 3.738.0(@aws-sdk/client-s3@3.738.0) @@ -25753,8 +25771,11 @@ snapshots: '@hcengineering/analytics': 0.7.3 '@hcengineering/analytics-service': 0.7.3(encoding@0.1.13) '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@smithy/node-http-handler': 4.0.2 '@tsconfig/node16': 1.0.4 @@ -25799,9 +25820,11 @@ snapshots: - aws-crt - babel-jest - babel-plugin-macros + - bufferutil - encoding - node-notifier - supports-color + - utf-8-validate '@rush-temp/pod-export@file:projects/pod-export.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(utf-8-validate@6.0.4)': dependencies: @@ -25810,6 +25833,9 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@tsconfig/node16': 1.0.4 @@ -25849,6 +25875,7 @@ snapshots: - '@jest/types' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - bufferutil @@ -25869,6 +25896,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/body-parser': 1.19.5 '@types/compression': 1.7.5 @@ -25909,7 +25937,7 @@ snapshots: - node-notifier - supports-color - '@rush-temp/pod-fulltext@file:projects/pod-fulltext.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)': + '@rush-temp/pod-fulltext@file:projects/pod-fulltext.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)': dependencies: '@hcengineering/analytics': 0.7.3 '@hcengineering/analytics-service': 0.7.3(encoding@0.1.13) @@ -25918,9 +25946,17 @@ snapshots: '@hcengineering/communication-sdk-types': 0.7.0 '@hcengineering/communication-server': 0.7.0 '@hcengineering/core': 0.7.3 + '@hcengineering/elastic': 0.7.0 '@hcengineering/hulylake-client': 0.7.3 + '@hcengineering/kafka': 0.7.0 + '@hcengineering/middleware': 0.7.1 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/postgres': 0.7.0 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@koa/cors': 5.0.0 '@types/jest': 29.5.12 @@ -25948,15 +25984,25 @@ snapshots: ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: + - '@aws-sdk/credential-providers' - '@babel/core' - '@jest/types' + - '@mongodb-js/zstd' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros + - bufferutil - encoding + - gcp-metadata + - kerberos + - mongodb-client-encryption - node-notifier + - snappy + - socks - supports-color + - utf-8-validate '@rush-temp/pod-github@file:projects/pod-github.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.3.7(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': dependencies: @@ -25967,9 +26013,14 @@ snapshots: '@hcengineering/client-resources': 0.7.3 '@hcengineering/collaborator-client': 0.7.3 '@hcengineering/core': 0.7.3 + '@hcengineering/minio': 0.7.0 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 '@hcengineering/query': 0.7.3 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/text-markdown': 0.7.3 @@ -26042,6 +26093,7 @@ snapshots: - '@mongodb-js/zstd' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - bufferutil @@ -26070,8 +26122,12 @@ snapshots: '@hcengineering/communication-rest-client': 0.7.0 '@hcengineering/communication-sdk-types': 0.7.0 '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@tsconfig/node16': 1.0.4 '@types/cors': 2.8.17 @@ -26114,6 +26170,7 @@ snapshots: - '@jest/types' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - bufferutil @@ -26133,8 +26190,13 @@ snapshots: '@hcengineering/client': 0.7.3 '@hcengineering/client-resources': 0.7.3 '@hcengineering/core': 0.7.3 + '@hcengineering/datalake': 0.7.0 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/s3': 0.7.0 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@tsconfig/node16': 1.0.4 '@types/cors': 2.8.17 @@ -26169,6 +26231,7 @@ snapshots: - '@jest/types' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - bufferutil @@ -26186,8 +26249,11 @@ snapshots: '@hcengineering/communication-sdk-types': 0.7.0 '@hcengineering/communication-types': 0.7.0 '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@tsconfig/node16': 1.0.4 '@types/cors': 2.8.17 @@ -26225,6 +26291,7 @@ snapshots: - '@jest/types' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - bufferutil @@ -26244,6 +26311,7 @@ snapshots: '@hcengineering/analytics-service': 0.7.3(encoding@0.1.13) '@hcengineering/core': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@tsconfig/node16': 1.0.4 '@types/cors': 2.8.17 '@types/express': 4.17.21 @@ -26291,8 +26359,12 @@ snapshots: '@hcengineering/communication-sdk-types': 0.7.0 '@hcengineering/communication-types': 0.7.0 '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/jest': 29.5.12 '@types/node': 22.15.29 @@ -26318,6 +26390,7 @@ snapshots: - '@jest/types' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - bufferutil @@ -26373,13 +26446,16 @@ snapshots: - node-notifier - supports-color - '@rush-temp/pod-preview@file:projects/pod-preview.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)': + '@rush-temp/pod-preview@file:projects/pod-preview.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)': dependencies: '@hcengineering/analytics': 0.7.3 '@hcengineering/analytics-service': 0.7.3(encoding@0.1.13) '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/cors': 2.8.17 '@types/express': 4.17.21 @@ -26417,11 +26493,14 @@ snapshots: - '@jest/types' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros + - bufferutil - encoding - node-notifier - supports-color + - utf-8-validate '@rush-temp/pod-print@file:projects/pod-print.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)': dependencies: @@ -26430,6 +26509,8 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@tsconfig/node16': 1.0.4 '@types/cors': 2.8.17 @@ -26462,6 +26543,7 @@ snapshots: - '@jest/types' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - bare-buffer @@ -26480,8 +26562,10 @@ snapshots: '@hcengineering/communication-sdk-types': 0.7.0 '@hcengineering/communication-types': 0.7.0 '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@temporalio/client': 1.12.3 '@tsconfig/node16': 1.0.4 @@ -26521,16 +26605,24 @@ snapshots: - supports-color - utf-8-validate - '@rush-temp/pod-server@file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)': + '@rush-temp/pod-server@file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.3)': dependencies: '@hcengineering/account-client': 0.7.3 '@hcengineering/analytics': 0.7.3 '@hcengineering/analytics-service': 0.7.3(encoding@0.1.13) '@hcengineering/communication-server': 0.7.0 '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 + '@hcengineering/middleware': 0.7.1 + '@hcengineering/minio': 0.7.0 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/postgres': 0.7.0 '@hcengineering/rpc': 0.7.3 + '@hcengineering/server': 0.7.0 + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/body-parser': 1.19.5 '@types/cors': 2.8.17 @@ -26566,17 +26658,24 @@ snapshots: utf-8-validate: 6.0.4 ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: + - '@aws-sdk/credential-providers' - '@babel/core' - '@jest/types' + - '@mongodb-js/zstd' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - encoding + - gcp-metadata + - kerberos + - mongodb-client-encryption - node-notifier + - socks - supports-color - '@rush-temp/pod-sign@file:projects/pod-sign.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)': + '@rush-temp/pod-sign@file:projects/pod-sign.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)': dependencies: '@hcengineering/account-client': 0.7.3 '@hcengineering/analytics-service': 0.7.3(encoding@0.1.13) @@ -26585,6 +26684,10 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server': 0.7.0 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@signpdf/placeholder-pdf-lib': 3.2.4(pdf-lib@1.17.1) '@signpdf/signer-p12': 3.2.4(node-forge@1.3.1) @@ -26622,8 +26725,10 @@ snapshots: - '@mongodb-js/zstd' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros + - bufferutil - encoding - gcp-metadata - kerberos @@ -26632,6 +26737,7 @@ snapshots: - snappy - socks - supports-color + - utf-8-validate '@rush-temp/pod-stats@file:projects/pod-stats.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)': dependencies: @@ -26640,6 +26746,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@koa/cors': 5.0.0 '@types/jest': 29.5.12 @@ -26685,8 +26792,13 @@ snapshots: '@hcengineering/client': 0.7.3 '@hcengineering/client-resources': 0.7.3 '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@telegraf/entity': 0.5.0 @@ -26726,6 +26838,7 @@ snapshots: - '@mongodb-js/zstd' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - bufferutil @@ -26749,8 +26862,12 @@ snapshots: '@hcengineering/client': 0.7.3 '@hcengineering/client-resources': 0.7.3 '@hcengineering/core': 0.7.3 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@tsconfig/node16': 1.0.4 '@types/cors': 2.8.17 @@ -26793,6 +26910,7 @@ snapshots: - '@mongodb-js/zstd' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - bufferutil @@ -26818,9 +26936,12 @@ snapshots: '@hcengineering/communication-types': 0.7.0 '@hcengineering/core': 0.7.3 '@hcengineering/hulylake-client': 0.7.3 + '@hcengineering/kafka': 0.7.0 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 '@hcengineering/retry': 0.7.3 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@tsconfig/node16': 1.0.4 '@types/jest': 29.5.12 @@ -26864,7 +26985,9 @@ snapshots: '@rush-temp/pod-worker@file:projects/pod-worker.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(webpack-cli@5.1.4)': dependencies: '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@temporalio/worker': 1.12.3(esbuild@0.25.9)(webpack-cli@5.1.4) '@temporalio/workflow': 1.12.3 '@tsconfig/node16': 1.0.4 @@ -26905,8 +27028,10 @@ snapshots: '@hcengineering/analytics': 0.7.3 '@hcengineering/analytics-service': 0.7.3(encoding@0.1.13) '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/jest': 29.5.12 '@types/node': 22.15.29 @@ -26943,36 +27068,6 @@ snapshots: - socks - supports-color - '@rush-temp/postgres@file:projects/postgres.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': - dependencies: - '@hcengineering/core': 0.7.3 - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@hcengineering/postgres-base': 0.7.6 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - postgres: 3.4.7 - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - babel-jest - - babel-plugin-macros - - esbuild - - node-notifier - - supports-color - - ts-node - '@rush-temp/preference-assets@file:projects/preference-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@hcengineering/platform': 0.7.3 @@ -27555,12 +27650,15 @@ snapshots: - supports-color - webpack - '@rush-temp/qms-doc-import-tool@file:projects/qms-doc-import-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))': + '@rush-temp/qms-doc-import-tool@file:projects/qms-doc-import-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(utf-8-validate@6.0.4)': dependencies: '@hcengineering/collaborator-client': 0.7.3 '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/domhandler': 2.4.5 '@types/htmlparser2': 3.10.7 @@ -27593,10 +27691,13 @@ snapshots: - '@jest/types' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros + - bufferutil - node-notifier - supports-color + - utf-8-validate '@rush-temp/qms-tests-sanity@file:projects/qms-tests-sanity.tgz': dependencies: @@ -27936,6 +28037,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/body-parser': 1.19.5 '@types/cors': 2.8.17 @@ -28122,40 +28224,6 @@ snapshots: - supports-color - ts-node - '@rush-temp/s3@file:projects/s3.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': - dependencies: - '@aws-sdk/client-s3': 3.738.0 - '@aws-sdk/lib-storage': 3.738.0(@aws-sdk/client-s3@3.738.0) - '@aws-sdk/s3-request-presigner': 3.738.0 - '@hcengineering/core': 0.7.3 - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@hcengineering/storage': 0.7.3 - '@smithy/node-http-handler': 4.0.2 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - aws-crt - - babel-jest - - babel-plugin-macros - - esbuild - - node-notifier - - supports-color - - ts-node - '@rush-temp/scripts@file:projects/scripts.tgz': dependencies: esbuild: 0.25.9 @@ -28168,6 +28236,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/text-core': 0.7.3 '@types/jest': 29.5.12 '@types/node': 22.15.29 @@ -28197,6 +28266,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28225,6 +28295,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28254,6 +28325,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28282,6 +28354,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -28310,6 +28383,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28338,6 +28412,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -28366,6 +28441,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28389,7 +28465,7 @@ snapshots: - supports-color - ts-node - '@rush-temp/server-backup@file:projects/server-backup.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': + '@rush-temp/server-backup@file:projects/server-backup.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4)': dependencies: '@hcengineering/analytics': 0.7.3 '@hcengineering/client': 0.7.3 @@ -28398,6 +28474,8 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/jest': 29.5.12 '@types/node': 22.15.29 @@ -28420,16 +28498,20 @@ snapshots: - '@jest/types' - babel-jest - babel-plugin-macros + - bufferutil - esbuild - node-notifier - supports-color - ts-node + - utf-8-validate '@rush-temp/server-calendar-resources@file:projects/server-calendar-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28459,6 +28541,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28489,6 +28572,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -28517,6 +28601,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28545,6 +28630,8 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server': 0.7.0 + '@hcengineering/server-core': 0.7.0 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/text-core': 0.7.3 '@types/jest': 29.5.12 @@ -28579,6 +28666,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28602,48 +28690,12 @@ snapshots: - supports-color - ts-node - '@rush-temp/server-client@file:projects/server-client.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4)': - dependencies: - '@hcengineering/account-client': 0.7.3 - '@hcengineering/client': 0.7.3 - '@hcengineering/client-resources': 0.7.3 - '@hcengineering/core': 0.7.3 - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@hcengineering/server-token': 0.7.2 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@types/uuid': 8.3.4 - '@types/ws': 8.5.11 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - typescript: 5.8.3 - ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4) - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - babel-jest - - babel-plugin-macros - - bufferutil - - esbuild - - node-notifier - - supports-color - - ts-node - - utf-8-validate - '@rush-temp/server-collaboration-resources@file:projects/server-collaboration-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -28672,6 +28724,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28701,6 +28754,7 @@ snapshots: '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 '@hcengineering/rank': 0.7.3 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -28729,6 +28783,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28757,6 +28812,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/jest': 29.5.12 '@types/node': 22.15.29 @@ -28787,6 +28843,7 @@ snapshots: dependencies: '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28811,49 +28868,12 @@ snapshots: - supports-color - ts-node - '@rush-temp/server-core@file:projects/server-core.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': - dependencies: - '@hcengineering/analytics': 0.7.3 - '@hcengineering/communication-sdk-types': 0.7.0 - '@hcengineering/communication-types': 0.7.0 - '@hcengineering/core': 0.7.3 - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@hcengineering/query': 0.7.3 - '@hcengineering/rpc': 0.7.3 - '@hcengineering/server-token': 0.7.2 - '@hcengineering/storage': 0.7.3 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@types/uuid': 8.3.4 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - fast-equals: 5.2.2 - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - typescript: 5.8.3 - uuid: 8.3.2 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - babel-jest - - babel-plugin-macros - - esbuild - - node-notifier - - supports-color - - ts-node - '@rush-temp/server-document-resources@file:projects/server-document-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28884,6 +28904,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28913,6 +28934,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -28941,6 +28963,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28970,6 +28993,7 @@ snapshots: '@hcengineering/model': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -28998,6 +29022,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29026,6 +29051,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29054,6 +29080,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29082,6 +29109,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29110,6 +29138,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29139,6 +29168,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29167,6 +29197,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29195,6 +29226,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29230,6 +29262,7 @@ snapshots: '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 '@hcengineering/query': 0.7.3 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@hcengineering/storage': 0.7.3 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) @@ -29268,6 +29301,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29323,6 +29357,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29350,6 +29385,7 @@ snapshots: dependencies: '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29378,6 +29414,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29408,6 +29445,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29439,6 +29477,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/text-core': 0.7.3 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29468,6 +29507,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29496,8 +29536,12 @@ snapshots: '@hcengineering/communication-sdk-types': 0.7.0 '@hcengineering/communication-types': 0.7.0 '@hcengineering/core': 0.7.3 + '@hcengineering/kafka': 0.7.0 + '@hcengineering/middleware': 0.7.1 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server': 0.7.0 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@types/jest': 29.5.12 '@types/node': 22.15.29 @@ -29530,6 +29574,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29558,6 +29603,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29588,6 +29634,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29618,6 +29665,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29646,6 +29694,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29674,6 +29723,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29701,6 +29751,7 @@ snapshots: dependencies: '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29729,6 +29780,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29757,6 +29809,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29780,45 +29833,12 @@ snapshots: - supports-color - ts-node - '@rush-temp/server-storage@file:projects/server-storage.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)': - dependencies: - '@hcengineering/analytics': 0.7.3 - '@hcengineering/core': 0.7.3 - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@hcengineering/server-token': 0.7.2 - '@hcengineering/storage': 0.7.3 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - cross-env: 7.0.3 - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - '@swc/core' - - '@swc/wasm' - - babel-jest - - babel-plugin-macros - - esbuild - - node-notifier - - supports-color - '@rush-temp/server-tags-resources@file:projects/server-tags-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29847,6 +29867,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29875,6 +29896,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -29903,6 +29925,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29931,6 +29954,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/server-token': 0.7.2 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@types/jest': 29.5.12 @@ -29965,6 +29989,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -29993,6 +30018,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -30022,6 +30048,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/text-core': 0.7.3 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -30051,6 +30078,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -30074,16 +30102,23 @@ snapshots: - supports-color - ts-node - '@rush-temp/server-tool@file:projects/server-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(gcp-metadata@5.3.0(encoding@0.1.13))(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': + '@rush-temp/server-tool@file:projects/server-tool.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.25.9)(gcp-metadata@5.3.0(encoding@0.1.13))(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))(utf-8-validate@6.0.4)': dependencies: '@hcengineering/account-client': 0.7.3 '@hcengineering/client': 0.7.3 '@hcengineering/client-resources': 0.7.3 + '@hcengineering/collaboration': 0.7.0(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/core': 0.7.3 + '@hcengineering/minio': 0.7.0 '@hcengineering/model': 0.7.3 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 '@hcengineering/rank': 0.7.3 + '@hcengineering/server': 0.7.0 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/text-markdown': 0.7.3 @@ -30112,8 +30147,10 @@ snapshots: - '@babel/core' - '@jest/types' - '@mongodb-js/zstd' + - aws-crt - babel-jest - babel-plugin-macros + - bufferutil - esbuild - gcp-metadata - kerberos @@ -30127,12 +30164,14 @@ snapshots: - socks - supports-color - ts-node + - utf-8-validate '@rush-temp/server-tracker-resources@file:projects/server-tracker-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@hcengineering/text-core': 0.7.3 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -30162,6 +30201,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -30190,6 +30230,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -30218,6 +30259,7 @@ snapshots: dependencies: '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -30247,6 +30289,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) @@ -30275,6 +30318,7 @@ snapshots: '@hcengineering/core': 0.7.3 '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/server-core': 0.7.0 '@types/jest': 29.5.12 '@types/node': 22.15.29 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) @@ -30298,40 +30342,6 @@ snapshots: - supports-color - ts-node - '@rush-temp/server@file:projects/server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': - dependencies: - '@hcengineering/account-client': 0.7.3 - '@hcengineering/analytics': 0.7.3 - '@hcengineering/core': 0.7.3 - '@hcengineering/platform': 0.7.3 - '@hcengineering/platform-rig': 0.7.10 - '@hcengineering/rpc': 0.7.3 - '@hcengineering/server-token': 0.7.2 - '@types/jest': 29.5.12 - '@types/node': 22.15.29 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) - cross-env: 7.0.3 - eslint: 8.56.0 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) - eslint-plugin-import: 2.29.1(eslint@8.56.0) - eslint-plugin-n: 15.7.0(eslint@8.56.0) - eslint-plugin-promise: 6.1.1(eslint@8.56.0) - jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) - prettier: 3.2.5 - ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) - typescript: 5.8.3 - utf-8-validate: 6.0.4 - transitivePeerDependencies: - - '@babel/core' - - '@jest/types' - - babel-jest - - babel-plugin-macros - - esbuild - - node-notifier - - supports-color - - ts-node - '@rush-temp/setting-assets@file:projects/setting-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@hcengineering/platform': 0.7.3 @@ -31534,13 +31544,24 @@ snapshots: '@hcengineering/api-client': 0.7.3(bufferutil@4.0.8)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(utf-8-validate@6.0.4) '@hcengineering/client': 0.7.3 '@hcengineering/client-resources': 0.7.3 + '@hcengineering/collaboration': 0.7.0(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/communication-types': 0.7.0 '@hcengineering/core': 0.7.3 + '@hcengineering/datalake': 0.7.0 + '@hcengineering/elastic': 0.7.0 '@hcengineering/hulylake-client': 0.7.3 + '@hcengineering/kafka': 0.7.0 + '@hcengineering/minio': 0.7.0 '@hcengineering/model': 0.7.3 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/postgres': 0.7.0 '@hcengineering/retry': 0.7.3 + '@hcengineering/s3': 0.7.0 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@hcengineering/text': 0.7.3(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) '@hcengineering/text-core': 0.7.3 @@ -31586,6 +31607,7 @@ snapshots: - '@mongodb-js/zstd' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros - gcp-metadata @@ -32172,14 +32194,19 @@ snapshots: - supports-color - ts-node - '@rush-temp/workspace-service@file:projects/workspace-service.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))': + '@rush-temp/workspace-service@file:projects/workspace-service.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)': dependencies: '@hcengineering/account-client': 0.7.3 '@hcengineering/analytics': 0.7.3 '@hcengineering/core': 0.7.3 '@hcengineering/model': 0.7.3 + '@hcengineering/mongo': 0.7.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) '@hcengineering/platform': 0.7.3 '@hcengineering/platform-rig': 0.7.10 + '@hcengineering/postgres': 0.7.0 + '@hcengineering/server-client': 0.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@hcengineering/server-core': 0.7.0 + '@hcengineering/server-storage': 0.7.0 '@hcengineering/server-token': 0.7.2 '@koa/cors': 5.0.0 '@types/jest': 29.5.12 @@ -32206,14 +32233,24 @@ snapshots: ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: + - '@aws-sdk/credential-providers' - '@babel/core' - '@jest/types' + - '@mongodb-js/zstd' - '@swc/core' - '@swc/wasm' + - aws-crt - babel-jest - babel-plugin-macros + - bufferutil + - gcp-metadata + - kerberos + - mongodb-client-encryption - node-notifier + - snappy + - socks - supports-color + - utf-8-validate '@selderee/plugin-htmlparser2@0.11.0': dependencies: diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index e9b9022bfb3..f7a03f37316 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -64,39 +64,39 @@ // // "includeEmailInChangeFile": true // }, // - // { - // /** - // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). - // * - // * The "individualVersion" mode specifies that the projects will use "individual versioning". - // * This is the typical NPM model where each package has an independent version number - // * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the - // * packages, they otherwise don't have any special relationship. The version bumping will - // * depend on how developers answer the "rush change" questions for each package that - // * is changed. - // */ - // "definitionName": "individualVersion", - // - // "policyName": "MyRandomLibraries", - // - // /** - // * (Optional) This can be used to enforce that all packages in the set must share a common - // * major version number, e.g. because they are from the same major release branch. - // * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes - // * inappropriately. The minor/patch version parts will be bumped independently according - // * to the types of changes made to each project, according to the "rush change" command. - // */ - // "lockedMajor": 3, - // - // /** - // * (Optional) When publishing is managed by Rush, by default the "rush change" command will - // * request changes for any projects that are modified by a pull request. These change entries - // * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates - // * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects - // * belonging to this version policy. - // */ - // "exemptFromRushChange": false, - // - // // "includeEmailInChangeFile": true - // } + { + /** + * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). + * + * The "individualVersion" mode specifies that the projects will use "individual versioning". + * This is the typical NPM model where each package has an independent version number + * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the + * packages, they otherwise don't have any special relationship. The version bumping will + * depend on how developers answer the "rush change" questions for each package that + * is changed. + */ + "definitionName": "individualVersion", + + "policyName": "MyRandomLibraries", + + /** + * (Optional) This can be used to enforce that all packages in the set must share a common + * major version number, e.g. because they are from the same major release branch. + * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes + * inappropriately. The minor/patch version parts will be bumped independently according + * to the types of changes made to each project, according to the "rush change" command. + */ + "lockedMajor": 3, + + /** + * (Optional) When publishing is managed by Rush, by default the "rush change" command will + * request changes for any projects that are modified by a pull request. These change entries + * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates + * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects + * belonging to this version policy. + */ + "exemptFromRushChange": false + + // "includeEmailInChangeFile": true + } ] diff --git a/common/scripts/package.json b/common/scripts/package.json index 33f49b0b1cd..ad9c2a6159d 100644 --- a/common/scripts/package.json +++ b/common/scripts/package.json @@ -2,7 +2,8 @@ "name": "@hcengineering/scripts", "version": "0.7.0", "scripts": { - "format": "echo \"No format specified\"" + "format": "echo \"No format specified\"", + "safe-publish": "node safe-publish.js" }, "devDependencies": { "esbuild": "^0.25.9", diff --git a/common/scripts/safe-publish.js b/common/scripts/safe-publish.js new file mode 100755 index 00000000000..82a6fcad3e8 --- /dev/null +++ b/common/scripts/safe-publish.js @@ -0,0 +1,158 @@ +#!/usr/bin/env node + +/** + * Safe publish script that skips packages that are already published + * Usage: node safe-publish.js [--include ] + */ + +const { execSync, spawnSync } = require('child_process') +const https = require('https') +const http = require('http') + +/** + * Check if a package version exists on npm registry + */ +function checkPackageExists(packageName, version) { + return new Promise((resolve) => { + const registryUrl = process.env.NPM_REGISTRY || 'https://registry.npmjs.org' + const url = `${registryUrl}/${encodeURIComponent(packageName)}/${version}` + + const client = url.startsWith('https:') ? https : http + + const req = client.get(url, (res) => { + if (res.statusCode === 200) { + resolve(true) // Package version exists + } else { + resolve(false) // Package version doesn't exist + } + }) + + req.on('error', () => { + resolve(false) // Assume doesn't exist on error + }) + + req.setTimeout(5000, () => { + req.destroy() + resolve(false) + }) + }) +} + +/** + * Get list of packages that should be published from rush + */ +function getPublishablePackages(includePattern) { + try { + const output = execSync('rush list -p --json', { encoding: 'utf-8' }) + const config = JSON.parse(output) + + return config.projects.filter((project) => { + // Check if package should be published according to rush.json + const shouldPublish = project.shouldPublish === true + + // Skip if no package name + if (!project.name) { + return false + } + + // If includePattern is specified, filter by it + if (includePattern && !project.name.includes(includePattern)) { + return false + } + + return shouldPublish && project.name.startsWith('@hcengineering') + }) + } catch (err) { + console.error('Error getting package list:', err.message) + return [] + } +} + +/** + * Publish a single package + */ +function publishPackage(packagePath, packageName) { + try { + console.log(`Publishing ${packageName}...`) + execSync('npm publish', { + cwd: packagePath, + stdio: 'inherit', + encoding: 'utf-8' + }) + console.log(`✓ Successfully published ${packageName}`) + return true + } catch (err) { + console.error(`✗ Failed to publish ${packageName}:`, err.message) + return false + } +} + +/** + * Main function + */ +async function main() { + const args = process.argv.slice(2) + let includePattern = null + + // Parse arguments + for (let i = 0; i < args.length; i++) { + if (args[i] === '--include' && i + 1 < args.length) { + includePattern = args[i + 1] + i++ + } + } + + console.log('Fetching publishable packages...') + const packages = getPublishablePackages(includePattern) + + if (packages.length === 0) { + console.log('No packages found to publish') + return + } + + console.log(`Found ${packages.length} package(s) to check`) + console.log('='.repeat(80)) + + let published = 0 + let skipped = 0 + let failed = 0 + + for (const pkg of packages) { + const packageName = pkg.name + const version = pkg.version + const packagePath = pkg.fullPath + + console.log(`\nChecking ${packageName}@${version}...`) + + const exists = await checkPackageExists(packageName, version) + + if (exists) { + console.log(`⊘ Skipping ${packageName}@${version} (already published)`) + skipped++ + } else { + console.log(`→ ${packageName}@${version} not published, publishing now...`) + const success = publishPackage(packagePath, packageName) + if (success) { + published++ + } else { + failed++ + } + } + } + + console.log('\n' + '='.repeat(80)) + console.log('Summary:') + console.log(` Published: ${published}`) + console.log(` Skipped: ${skipped}`) + console.log(` Failed: ${failed}`) + console.log('='.repeat(80)) + + if (failed > 0) { + process.exit(1) + } +} + +main().catch((err) => { + console.error('Error:', err) + process.exit(1) +}) diff --git a/plugins/guest/package.json b/plugins/guest/package.json index 540a7924871..12a15664ddc 100644 --- a/plugins/guest/package.json +++ b/plugins/guest/package.json @@ -40,5 +40,9 @@ "@hcengineering/core": "^0.7.3", "@hcengineering/platform": "^0.7.3", "@hcengineering/ui": "^0.7.0" + }, + "repository": "https://github.com/hcengineering/platform", + "publishConfig": { + "access": "public" } } diff --git a/pods/fulltext/package.json b/pods/fulltext/package.json index 0cd0d9d2e07..a86e6744940 100644 --- a/pods/fulltext/package.json +++ b/pods/fulltext/package.json @@ -70,7 +70,7 @@ "@hcengineering/server-indexer": "^0.7.0", "@hcengineering/elastic": "^0.7.0", "@hcengineering/server-collaboration": "^0.7.0", - "@hcengineering/middleware": "^0.7.0", + "@hcengineering/middleware": "^0.7.1", "@hcengineering/server-client": "^0.7.0", "@hcengineering/server-storage": "^0.7.0", "@hcengineering/postgres": "^0.7.0", diff --git a/pods/fulltext/src/__tests__/indexing.spec.ts b/pods/fulltext/src/__tests__/indexing.spec.ts index 06c0b9b2d21..b20659bb5c9 100644 --- a/pods/fulltext/src/__tests__/indexing.spec.ts +++ b/pods/fulltext/src/__tests__/indexing.spec.ts @@ -33,7 +33,7 @@ import { dbConfig, dbUrl, elasticIndexName, model, prepare, preparePipeline } fr prepare() jest.mock('franc-min', () => ({ franc: () => 'en' }), { virtual: true }) -jest.setTimeout(500000) +jest.setTimeout(30000) class TestWorkspaceManager extends WorkspaceManager { public async getWorkspaceInfo (ctx: MeasureContext, token?: string): Promise { diff --git a/pods/fulltext/src/workspace.ts b/pods/fulltext/src/workspace.ts index 269c1469070..6df07462b31 100644 --- a/pods/fulltext/src/workspace.ts +++ b/pods/fulltext/src/workspace.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/unbound-method */ +import { Api as CommunicationApi } from '@hcengineering/communication-server' import core, { type Class, type Doc, @@ -15,6 +16,7 @@ import core, { WorkspaceEvent, type WorkspaceIds } from '@hcengineering/core' +import { type HulylakeClient } from '@hcengineering/hulylake-client' import { ContextNameMiddleware, DBAdapterInitMiddleware, @@ -38,8 +40,6 @@ import { import { FullTextIndexPipeline } from '@hcengineering/server-indexer' import { getConfig } from '@hcengineering/server-pipeline' import { generateToken } from '@hcengineering/server-token' -import { Api as CommunicationApi } from '@hcengineering/communication-server' -import { type HulylakeClient } from '@hcengineering/hulylake-client' import { fulltextModelFilter } from './utils' diff --git a/pods/server/package.json b/pods/server/package.json index ec04c1d952b..86df969fcd8 100644 --- a/pods/server/package.json +++ b/pods/server/package.json @@ -65,7 +65,7 @@ "@hcengineering/contact": "^0.7.0", "@hcengineering/core": "^0.7.3", "@hcengineering/kafka": "^0.7.0", - "@hcengineering/middleware": "^0.7.0", + "@hcengineering/middleware": "^0.7.1", "@hcengineering/minio": "^0.7.0", "@hcengineering/mongo": "^0.7.0", "@hcengineering/notification": "^0.7.0", diff --git a/rush.json b/rush.json index af0e41c5dc5..0a4b90929f0 100644 --- a/rush.json +++ b/rush.json @@ -338,7 +338,7 @@ { "packageName": "@hcengineering/highlight", "projectFolder": "packages/highlight", - "shouldPublish": false + "shouldPublish": true }, { "packageName": "@hcengineering/api-tests", @@ -350,11 +350,6 @@ "projectFolder": "packages/importer", "shouldPublish": false }, - { - "packageName": "@hcengineering/collaboration", - "projectFolder": "server/collaboration", - "shouldPublish": false - }, { "packageName": "@hcengineering/hls", "projectFolder": "packages/hls", @@ -455,26 +450,11 @@ "projectFolder": "dev/prod", "shouldPublish": false }, - { - "packageName": "@hcengineering/server-core", - "projectFolder": "server/core", - "shouldPublish": false - }, { "packageName": "@hcengineering/server-indexer", "projectFolder": "server/indexer", "shouldPublish": false }, - { - "packageName": "@hcengineering/server", - "projectFolder": "server/server", - "shouldPublish": false - }, - { - "packageName": "@hcengineering/server-storage", - "projectFolder": "server/server-storage", - "shouldPublish": false - }, { "packageName": "@hcengineering/server-pipeline", "projectFolder": "server/server-pipeline", @@ -498,7 +478,7 @@ { "packageName": "@hcengineering/onboard", "projectFolder": "plugins/onboard", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/onboard-assets", @@ -518,7 +498,7 @@ { "packageName": "@hcengineering/workbench-assets", "projectFolder": "plugins/workbench-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/workbench-resources", @@ -543,7 +523,7 @@ { "packageName": "@hcengineering/view-assets", "projectFolder": "plugins/view-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/view-resources", @@ -573,7 +553,7 @@ { "packageName": "@hcengineering/contact-assets", "projectFolder": "plugins/contact-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/contact-resources", @@ -588,7 +568,7 @@ { "packageName": "@hcengineering/task-assets", "projectFolder": "plugins/task-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/task-resources", @@ -633,7 +613,7 @@ { "packageName": "@hcengineering/chunter-assets", "projectFolder": "plugins/chunter-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/chunter-resources", @@ -653,7 +633,7 @@ { "packageName": "@hcengineering/recruit-assets", "projectFolder": "plugins/recruit-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/recruit-resources", @@ -725,21 +705,6 @@ "projectFolder": "server-plugins/contact-resources", "shouldPublish": false }, - { - "packageName": "@hcengineering/mongo", - "projectFolder": "server/mongo", - "shouldPublish": false - }, - { - "packageName": "@hcengineering/postgres", - "projectFolder": "server/postgres", - "shouldPublish": false - }, - { - "packageName": "@hcengineering/elastic", - "projectFolder": "server/elastic", - "shouldPublish": false - }, { "packageName": "@hcengineering/pod-front", "projectFolder": "pods/front", @@ -823,7 +788,7 @@ { "packageName": "@hcengineering/activity-assets", "projectFolder": "plugins/activity-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/activity-resources", @@ -843,7 +808,7 @@ { "packageName": "@hcengineering/setting-assets", "projectFolder": "plugins/setting-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/setting-resources", @@ -893,7 +858,7 @@ { "packageName": "@hcengineering/attachment-assets", "projectFolder": "plugins/attachment-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/attachment-resources", @@ -963,7 +928,7 @@ { "packageName": "@hcengineering/inventory-assets", "projectFolder": "plugins/inventory-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/inventory-resources", @@ -978,7 +943,7 @@ { "packageName": "@hcengineering/templates-assets", "projectFolder": "plugins/templates-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/templates-resources", @@ -1010,11 +975,6 @@ "projectFolder": "server/tool", "shouldPublish": false }, - { - "packageName": "@hcengineering/server-client", - "projectFolder": "server/client", - "shouldPublish": false - }, { "packageName": "@hcengineering/tests-sanity", "projectFolder": "tests/sanity", @@ -1048,7 +1008,7 @@ { "packageName": "@hcengineering/notification-assets", "projectFolder": "plugins/notification-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/notification-resources", @@ -1203,7 +1163,7 @@ { "packageName": "@hcengineering/calendar-assets", "projectFolder": "plugins/calendar-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/calendar-resources", @@ -1304,7 +1264,7 @@ { "packageName": "@hcengineering/board-assets", "projectFolder": "plugins/board-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/board-resources", @@ -1341,11 +1301,6 @@ "projectFolder": "server-plugins/preference", "shouldPublish": false }, - { - "packageName": "@hcengineering/middleware", - "projectFolder": "server/middleware", - "shouldPublish": false - }, { "packageName": "@hcengineering/server-backup", "projectFolder": "server/backup", @@ -1384,7 +1339,7 @@ { "packageName": "@hcengineering/hr-assets", "projectFolder": "plugins/hr-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/hr-resources", @@ -1411,21 +1366,6 @@ "projectFolder": "server-plugins/hr-resources", "shouldPublish": false }, - { - "packageName": "@hcengineering/minio", - "projectFolder": "server/minio", - "shouldPublish": false - }, - { - "packageName": "@hcengineering/s3", - "projectFolder": "server/s3", - "shouldPublish": false - }, - { - "packageName": "@hcengineering/datalake", - "projectFolder": "server/datalake", - "shouldPublish": false - }, { "packageName": "@hcengineering/bitrix", "projectFolder": "plugins/bitrix", @@ -1524,7 +1464,7 @@ { "packageName": "@hcengineering/support-assets", "projectFolder": "plugins/support-assets", - "shouldPublish": true + "shouldPublish": false }, { "packageName": "@hcengineering/support-resources", @@ -2281,11 +2221,6 @@ "projectFolder": "server-plugins/process-resources", "shouldPublish": false }, - { - "packageName": "@hcengineering/kafka", - "projectFolder": "server/kafka", - "shouldPublish": false - }, { "packageName": "@hcengineering/pod-mail-worker", "projectFolder": "services/mail/pod-mail-worker", diff --git a/server/client/.eslintrc.js b/server/client/.eslintrc.js deleted file mode 100644 index ce90fb9646f..00000000000 --- a/server/client/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/client/.npmignore b/server/client/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/client/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/client/config/rig.json b/server/client/config/rig.json deleted file mode 100644 index 78cc5a17334..00000000000 --- a/server/client/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/server/client/jest.config.js b/server/client/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/client/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/client/package.json b/server/client/package.json deleted file mode 100644 index c652a424ae0..00000000000 --- a/server/client/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@hcengineering/server-client", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --forceExit", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "@types/uuid": "^8.3.1", - "@types/ws": "^8.5.11", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/node": "^22.15.29" - }, - "dependencies": { - "@hcengineering/platform": "^0.7.3", - "@hcengineering/core": "^0.7.3", - "@hcengineering/client-resources": "^0.7.3", - "@hcengineering/client": "^0.7.3", - "@hcengineering/account-client": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "@hcengineering/server-token": "^0.7.0", - "ws": "^8.18.2" - } -} diff --git a/server/client/src/account.ts b/server/client/src/account.ts deleted file mode 100644 index 5c921c8ac82..00000000000 --- a/server/client/src/account.ts +++ /dev/null @@ -1,122 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import { getClient as getAccountClientRaw, type AccountClient } from '@hcengineering/account-client' -import { getMetadata } from '@hcengineering/platform' - -import plugin from './plugin' - -export interface WorkspaceLoginInfo extends LoginInfo { - workspace: string - endpoint: string -} -export interface LoginInfo { - account: string - token: string -} - -const connectionErrorCodes = ['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND'] - -export function getAccountClient (token?: string, retryTimeoutMs?: number): AccountClient { - const accountsUrl = getMetadata(plugin.metadata.Endpoint) - - return getAccountClientRaw(accountsUrl, token, retryTimeoutMs) -} - -const externalRegions = process.env.EXTERNAL_REGIONS?.split(';') ?? [] - -/** - * Retrieves the transactor endpoint for a given token and kind. - * - * @param token - The authorization token. - * @param kind - The type of endpoint to retrieve. Can be 'internal', 'external', or 'byregion'. Defaults to 'byregion'. - * @param timeout - The timeout duration in milliseconds. Defaults to -1 (no timeout). - * @returns A promise that resolves to the transactor endpoint URL as a string. - * @throws Will throw an error if the request fails or if the timeout is reached. - */ -export async function getTransactorEndpoint ( - token: string, - kind: 'internal' | 'external' | 'byregion' = 'byregion', - timeout: number = -1 -): Promise { - const accountClient = getAccountClient(token, 30000) - const st = Date.now() - while (true) { - try { - const workspaceInfo = await accountClient.selectWorkspace('', kind, externalRegions) - if (workspaceInfo === undefined) { - throw new Error('Workspace not found') - } - return workspaceInfo.endpoint - } catch (err: any) { - if (timeout > 0 && st + timeout < Date.now()) { - // Timeout happened - throw err - } - if (connectionErrorCodes.includes(err?.cause?.code)) { - await new Promise((resolve) => setTimeout(resolve, 1000)) - } else { - throw err - } - } - } -} - -export function withRetry

( - f: (...params: P) => Promise, - shouldFail: (err: any, attempt: number) => boolean, - intervalMs: number = 1000 -): (...params: P) => Promise { - return async function (...params: P): Promise { - let attempt = 0 - while (true) { - try { - return await f(...params) - } catch (err: any) { - if (shouldFail(err, attempt)) { - throw err - } - - attempt++ - await new Promise((resolve) => setTimeout(resolve, intervalMs)) - } - } - } -} - -export function withRetryConnUntilTimeout

( - f: (...params: P) => Promise, - timeoutMs: number = 5000 -): (...params: P) => Promise { - const timeout = Date.now() + timeoutMs - const shouldFail = (err: any): boolean => !connectionErrorCodes.includes(err?.cause?.code) || timeout < Date.now() - - return withRetry(f, shouldFail) -} - -export function withRetryConnUntilSuccess

( - f: (...params: P) => Promise -): (...params: P) => Promise { - const shouldFail = (err: any): boolean => { - const res = !connectionErrorCodes.includes(err?.cause?.code) - - if (res) { - console.error('Failing withRetryConnUntilSuccess with error cause:', err?.cause) - } - - return res - } - - return withRetry(f, shouldFail) -} diff --git a/server/client/src/blob.ts b/server/client/src/blob.ts deleted file mode 100644 index 65171c86119..00000000000 --- a/server/client/src/blob.ts +++ /dev/null @@ -1,108 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type MeasureContext, type WorkspaceIds } from '@hcengineering/core' -import type { StorageAdapter } from '@hcengineering/server-core' -import { Buffer } from 'node:buffer' - -// Will use temporary file to store huge content into -export class BlobClient { - index: number - constructor ( - readonly storageAdapter: StorageAdapter, - readonly workspace: WorkspaceIds - ) { - this.index = 0 - } - - async checkFile (ctx: MeasureContext, name: string): Promise { - const obj = await this.storageAdapter.stat(ctx, this.workspace, name) - if (obj !== undefined) { - return true - } - return false - } - - async writeTo ( - ctx: MeasureContext, - name: string, - size: number, - writable: { - write: (buffer: Buffer, cb: (err?: any) => void) => void - end: (cb: () => void) => void - } - ): Promise { - let written = 0 - const chunkSize = 50 * 1024 * 1024 - - // Use ranges to iterave through file with retry if required. - while (written < size) { - let i = 0 - for (; i < 5; i++) { - try { - const chunks: Buffer[] = [] - const readable = await this.storageAdapter.partial(ctx, this.workspace, name, written, chunkSize) - await new Promise((resolve) => { - readable.on('data', (chunk) => { - chunks.push(chunk) - }) - readable.on('end', () => { - readable.destroy() - resolve() - }) - }) - const chunk = Buffer.concat(chunks) - - await new Promise((resolve, reject) => { - writable.write(chunk, (err) => { - if (err != null) { - reject(err) - } - resolve() - }) - }) - - written += chunk.length - break - } catch (err: any) { - if ( - err?.code === 'NoSuchKey' || - err?.code === 'NotFound' || - err?.message === 'No such key' || - err?.Code === 'NoSuchKey' - ) { - ctx.info('No such key', { name }) - return - } - if (i > 4) { - await new Promise((resolve) => { - writable.end(resolve) - }) - throw err - } - await new Promise((resolve) => setTimeout(resolve, 10)) - // retry - } - } - } - await new Promise((resolve) => { - writable.end(resolve) - }) - } - - async upload (ctx: MeasureContext, name: string, size: number, contentType: string, buffer: Buffer): Promise { - await this.storageAdapter.put(ctx, this.workspace, name, buffer, contentType, size) - } -} diff --git a/server/client/src/client.ts b/server/client/src/client.ts deleted file mode 100644 index fa9f355587d..00000000000 --- a/server/client/src/client.ts +++ /dev/null @@ -1,72 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import client, { clientId } from '@hcengineering/client' -import { type Client, type LoadModelResponse, type Tx } from '@hcengineering/core' -import { addLocation, getMetadata, getResource, setMetadata } from '@hcengineering/platform' -import crypto from 'node:crypto' -import plugin from './plugin' - -/** - * @public - * - * If connectTimeout is set, connect will try to connect only specified amount of time, and will return failure if failed. - */ -export async function createClient ( - transactorUrl: string, - token: string, - model?: Tx[], - connectTimeout: number = 0 -): Promise { - // We need to override default factory with 'ws' one. - // eslint-disable-next-line - const WebSocket = require('ws') - - setMetadata(client.metadata.UseBinaryProtocol, true) - setMetadata(client.metadata.UseProtocolCompression, true) - setMetadata(client.metadata.ConnectionTimeout, connectTimeout) - - setMetadata(client.metadata.ClientSocketFactory, (url) => { - const socket = new WebSocket(url, { - headers: { - 'User-Agent': getMetadata(plugin.metadata.UserAgent) ?? 'Anticrm Client' - } - }) - return socket - }) - addLocation(clientId, () => import('@hcengineering/client-resources')) - - if (model !== undefined) { - let prev = '' - const hashes = model.map((it) => { - const h = crypto.createHash('sha1') - h.update(prev) - h.update(JSON.stringify(it)) - prev = h.digest('hex') - return prev - }) - setMetadata(client.metadata.OverridePersistenceStore, { - load: async () => ({ - hash: hashes[hashes.length - 1], - transactions: model, - full: true - }), - store: async (model: LoadModelResponse) => {} - }) - } - - const clientFactory = await getResource(client.function.GetClient) - return await clientFactory(token, transactorUrl) -} diff --git a/server/client/src/index.ts b/server/client/src/index.ts deleted file mode 100644 index eacb0dbe921..00000000000 --- a/server/client/src/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021, 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import plugin from './plugin' - -export default plugin - -export * from './account' -export * from './blob' -export * from './client' -export * from './token' diff --git a/server/client/src/plugin.ts b/server/client/src/plugin.ts deleted file mode 100644 index 26f5eb747ea..00000000000 --- a/server/client/src/plugin.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type Metadata, plugin, type Plugin } from '@hcengineering/platform' - -/** - * @public - */ -export const toolId = 'server-client' as Plugin - -/** - * @public - */ -const serverClientPlugin = plugin(toolId, { - metadata: { - Endpoint: '' as Metadata, // Account URL endpoint - UserAgent: '' as Metadata - } -}) - -export default serverClientPlugin diff --git a/server/client/src/token.ts b/server/client/src/token.ts deleted file mode 100644 index 0ee0cff137a..00000000000 --- a/server/client/src/token.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { type Token, decodeToken } from '@hcengineering/server-token' -import { type IncomingHttpHeaders } from 'http' - -const extractCookieToken = (cookie?: string): string | null => { - if (cookie === undefined || cookie === null) { - return null - } - - const cookies = cookie.split(';') - const tokenCookie = cookies.find((cookie) => cookie.toLocaleLowerCase().includes('token')) - if (tokenCookie === undefined) { - return null - } - - const encodedToken = tokenCookie.split('=')[1] - if (encodedToken === undefined) { - return null - } - - return encodedToken -} - -const extractAuthorizationToken = (authorization?: string): string | null => { - if (authorization === undefined || authorization === null) { - return null - } - const encodedToken = authorization.split(' ')[1] - - if (encodedToken === undefined) { - return null - } - - return encodedToken -} - -export function readToken (headers: IncomingHttpHeaders): string | undefined { - try { - return extractAuthorizationToken(headers.authorization) ?? extractCookieToken(headers.cookie) ?? undefined - } catch { - return undefined - } -} - -export function extractToken (headers: IncomingHttpHeaders): Token | undefined { - try { - const tokenStr = readToken(headers) - - return tokenStr !== undefined ? decodeToken(tokenStr) : undefined - } catch { - return undefined - } -} diff --git a/server/client/tsconfig.json b/server/client/tsconfig.json deleted file mode 100644 index c6a877cf6c3..00000000000 --- a/server/client/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/collaboration/.eslintrc.js b/server/collaboration/.eslintrc.js deleted file mode 100644 index ce90fb9646f..00000000000 --- a/server/collaboration/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/collaboration/.npmignore b/server/collaboration/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/collaboration/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/collaboration/config/rig.json b/server/collaboration/config/rig.json deleted file mode 100644 index 78cc5a17334..00000000000 --- a/server/collaboration/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/server/collaboration/jest.config.js b/server/collaboration/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/collaboration/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/collaboration/package.json b/server/collaboration/package.json deleted file mode 100644 index bb49763d836..00000000000 --- a/server/collaboration/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "@hcengineering/collaboration", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Hardcore Engineering Inc.", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --forceExit", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@types/node": "^22.15.29", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" - }, - "dependencies": { - "@hcengineering/core": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "@hcengineering/text": "^0.7.3", - "@hcengineering/text-ydoc": "^0.7.3", - "base64-js": "^1.5.1", - "yjs": "^13.6.27" - } -} diff --git a/server/collaboration/src/index.ts b/server/collaboration/src/index.ts deleted file mode 100644 index 88cac915023..00000000000 --- a/server/collaboration/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './storage' -export * from './ydoc' diff --git a/server/collaboration/src/storage.ts b/server/collaboration/src/storage.ts deleted file mode 100644 index e0dc833f3d0..00000000000 --- a/server/collaboration/src/storage.ts +++ /dev/null @@ -1,126 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Blob, - type CollaborativeDoc, - type Ref, - type WorkspaceIds, - type Markup, - type MarkupBlobRef, - type MeasureContext, - generateId, - makeCollabJsonId, - makeCollabYdocId -} from '@hcengineering/core' -import { type StorageAdapter } from '@hcengineering/server-core' -import { yDocToMarkup } from '@hcengineering/text-ydoc' -import { Doc as YDoc } from 'yjs' - -import { yDocFromBuffer, yDocToBuffer } from './ydoc' - -/** @public */ -export async function loadCollabYdoc ( - ctx: MeasureContext, - storageAdapter: StorageAdapter, - wsIds: WorkspaceIds, - doc: CollaborativeDoc | MarkupBlobRef -): Promise { - const blobId = typeof doc === 'string' ? doc : makeCollabYdocId(doc) - - const blob = await storageAdapter.stat(ctx, wsIds, blobId) - if (blob === undefined) { - return undefined - } - - if (!blob.contentType.includes('application/ydoc')) { - ctx.warn('invalid content type', { contentType: blob.contentType }) - } - - // no need to apply gc because we load existing document - // it is either already gc-ed, or gc not needed and it is disabled - const ydoc = new YDoc({ guid: generateId(), gc: false }) - - const buffer = await storageAdapter.read(ctx, wsIds, blobId) - return yDocFromBuffer(Buffer.concat(buffer), ydoc) -} - -/** @public */ -export async function saveCollabYdoc ( - ctx: MeasureContext, - storageAdapter: StorageAdapter, - wsIds: WorkspaceIds, - doc: CollaborativeDoc | MarkupBlobRef, - ydoc: YDoc -): Promise> { - const blobId = typeof doc === 'string' ? doc : makeCollabYdocId(doc) - - const buffer = yDocToBuffer(ydoc) - await storageAdapter.put(ctx, wsIds, blobId, buffer, 'application/ydoc', buffer.length) - - return blobId -} - -/** @public */ -export async function removeCollabYdoc ( - storageAdapter: StorageAdapter, - wsIds: WorkspaceIds, - collaborativeDocs: CollaborativeDoc[], - ctx: MeasureContext -): Promise { - const toRemove: string[] = collaborativeDocs.map(makeCollabYdocId) - if (toRemove.length > 0) { - await ctx.with('remove', {}, async () => { - await storageAdapter.remove(ctx, wsIds, toRemove) - }) - } -} - -/** @public */ -export async function loadCollabJson ( - ctx: MeasureContext, - storageAdapter: StorageAdapter, - wsIds: WorkspaceIds, - blobId: Ref -): Promise { - const blob = await storageAdapter.stat(ctx, wsIds, blobId) - if (blob === undefined) { - return undefined - } - - if (!blob.contentType.includes('application/json')) { - ctx.warn('invalid content type', { contentType: blob.contentType }) - } - - const buffer = await storageAdapter.read(ctx, wsIds, blobId) - return Buffer.concat(buffer as any).toString() -} - -/** @public */ -export async function saveCollabJson ( - ctx: MeasureContext, - storageAdapter: StorageAdapter, - wsIds: WorkspaceIds, - doc: CollaborativeDoc, - content: Markup | YDoc -): Promise> { - const blobId = makeCollabJsonId(doc) - - const markup = typeof content === 'string' ? content : yDocToMarkup(content, doc.objectAttr) - const buffer = Buffer.from(markup) - await storageAdapter.put(ctx, wsIds, blobId, buffer, 'application/json', buffer.length) - - return blobId -} diff --git a/server/collaboration/src/ydoc.ts b/server/collaboration/src/ydoc.ts deleted file mode 100644 index b20cd907e8d..00000000000 --- a/server/collaboration/src/ydoc.ts +++ /dev/null @@ -1,82 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { generateId } from '@hcengineering/core' -import { - AbstractType as YAbstractType, - Doc as YDoc, - applyUpdate, - encodeStateAsUpdate, - XmlElement as YXmlElement -} from 'yjs' - -export { XmlElement as YXmlElement, XmlText as YXmlText, AbstractType as YAbstractType } from 'yjs' - -/** @public */ -export function yDocFromBuffer (buffer: Buffer, ydoc?: YDoc): YDoc { - ydoc ??= new YDoc({ guid: generateId(), gc: false }) - try { - const uint8arr = new Uint8Array(buffer) - applyUpdate(ydoc, uint8arr) - return ydoc - } catch (err) { - throw new Error('Failed to apply ydoc update', { cause: err }) - } -} - -/** @public */ -export function yDocToBuffer (ydoc: YDoc): Buffer { - const update = encodeStateAsUpdate(ydoc) - return Buffer.from(update.buffer) -} - -/** @public */ -export function yDocCopyXmlField (ydoc: YDoc, source: string, target: string): void { - const srcField = ydoc.getXmlFragment(source) - const dstField = ydoc.getXmlFragment(target) - - ydoc.transact((tr) => { - // similar to XmlFragment's clone method - dstField.delete(0, dstField.length) - dstField.insert(0, srcField.toArray().map((item) => (item instanceof YAbstractType ? item.clone() : item)) as any) - }) -} - -/** - * @public - * Replaces standard clone method that only copies string attributes - * https://github.com/yjs/yjs/blob/main/src/types/YXmlElement.js#L97 - * @param src YXmlElement - * @returns YXmlElement - */ -export function yXmlElementClone (src: YXmlElement): YXmlElement { - const el = new YXmlElement(src.nodeName) - const attrs = src.getAttributes() - - Object.entries(attrs).forEach(([key, value]) => { - el.setAttribute(key, value as any) - }) - - el.insert( - 0, - src - .toArray() - .map((item) => - item instanceof YAbstractType ? (item instanceof YXmlElement ? yXmlElementClone(item) : item.clone()) : item - ) as any - ) - - return el -} diff --git a/server/collaboration/tsconfig.json b/server/collaboration/tsconfig.json deleted file mode 100644 index c6a877cf6c3..00000000000 --- a/server/collaboration/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/core/.eslintrc.js b/server/core/.eslintrc.js deleted file mode 100644 index ce90fb9646f..00000000000 --- a/server/core/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/core/.npmignore b/server/core/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/core/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/core/CHANGELOG.json b/server/core/CHANGELOG.json deleted file mode 100644 index b182d8a33df..00000000000 --- a/server/core/CHANGELOG.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@hcengineering/server-core", - "entries": [ - { - "version": "0.7.0", - "tag": "@hcengineering/server-core_v0.6.0", - "date": "Sun, 08 Aug 2021 10:14:57 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.3` to `~0.6.4`" - } - ] - } - } - ] -} diff --git a/server/core/CHANGELOG.md b/server/core/CHANGELOG.md deleted file mode 100644 index 7a1b917dea7..00000000000 --- a/server/core/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Change Log - @hcengineering/server-core - -This log was last generated on Sun, 08 Aug 2021 10:14:57 GMT and should not be manually modified. - -## 0.6.0 -Sun, 08 Aug 2021 10:14:57 GMT - -_Initial release_ - diff --git a/server/core/config/rig.json b/server/core/config/rig.json deleted file mode 100644 index 78cc5a17334..00000000000 --- a/server/core/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/server/core/jest.config.js b/server/core/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/core/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/core/package.json b/server/core/package.json deleted file mode 100644 index d716ba781ab..00000000000 --- a/server/core/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@hcengineering/server-core", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --forceExit", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@types/node": "^22.15.29", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/uuid": "^8.3.1" - }, - "dependencies": { - "@hcengineering/analytics": "^0.7.3", - "@hcengineering/communication-sdk-types": "^0.7.0", - "@hcengineering/communication-types": "^0.7.0", - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "@hcengineering/query": "^0.7.3", - "@hcengineering/rpc": "^0.7.3", - "@hcengineering/server-token": "^0.7.0", - "@hcengineering/storage": "^0.7.3", - "fast-equals": "^5.2.2", - "uuid": "^8.3.2" - } -} diff --git a/server/core/src/adapter.ts b/server/core/src/adapter.ts deleted file mode 100644 index 89776f06fbf..00000000000 --- a/server/core/src/adapter.ts +++ /dev/null @@ -1,124 +0,0 @@ -// -// Copyright © 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Class, - type Doc, - type DocumentQuery, - type Domain, - type FieldIndexConfig, - type FindResult, - type Hierarchy, - type LowLevelStorage, - type MeasureContext, - type ModelDb, - type Ref, - type Tx, - type TxResult, - type WorkspaceIds, - type WorkspaceUuid -} from '@hcengineering/core' -import { type StorageAdapter } from './storage' -import type { ServerFindOptions } from './types' - -export interface DomainHelperOperations { - create: (domain: Domain) => Promise - exists: (domain: Domain) => Promise - - listDomains: () => Promise> - createIndex: (domain: Domain, value: string | FieldIndexConfig, options?: { name: string }) => Promise - dropIndex: (domain: Domain, name: string) => Promise - listIndexes: (domain: Domain) => Promise<{ name: string }[]> - - // Could return 0 even if it has documents - estimatedCount: (domain: Domain) => Promise -} - -export interface DomainHelper { - checkDomain: ( - ctx: MeasureContext, - domain: Domain, - documents: number, - operations: DomainHelperOperations - ) => Promise -} - -export type DbAdapterHandler = ( - domain: Domain, - event: 'add' | 'update' | 'delete' | 'read', - count: number, - helper: DomainHelperOperations -) => void - -export interface RawFindIterator { - find: (ctx: MeasureContext) => Promise - close: () => Promise -} -/** - * @public - */ -export interface DbAdapter extends LowLevelStorage { - init?: ( - ctx: MeasureContext, - contextVars: Record, - domains?: string[], - excludeDomains?: string[] - ) => Promise - - helper?: () => DomainHelperOperations - - reserveContext?: (id: string) => () => void - close: () => Promise - findAll: ( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: ServerFindOptions - ) => Promise> - - tx: (ctx: MeasureContext, ...tx: Tx[]) => Promise - - // Allow to register a handler to listen for domain operations - on?: (handler: DbAdapterHandler) => void - - rawFind: (ctx: MeasureContext, domain: Domain) => RawFindIterator -} - -/** - * @public - */ -export interface TxAdapter extends DbAdapter { - getModel: (ctx: MeasureContext) => Promise -} - -/** - * Adpater to delete a selected workspace and all its data. - * @public - */ -export interface WorkspaceDestroyAdapter { - deleteWorkspace: (ctx: MeasureContext, workspace: WorkspaceUuid, dataId?: string) => Promise -} - -/** - * @public - */ -export type DbAdapterFactory = ( - ctx: MeasureContext, - hierarchy: Hierarchy, - url: string, - workspaceId: WorkspaceIds, - modelDb: ModelDb, - storage?: StorageAdapter -) => Promise diff --git a/server/core/src/base.ts b/server/core/src/base.ts deleted file mode 100644 index 85133ca02ee..00000000000 --- a/server/core/src/base.ts +++ /dev/null @@ -1,173 +0,0 @@ -// -// Copyright © 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Class, - type Doc, - type DocumentQuery, - type Domain, - type DomainParams, - type FindOptions, - type FindResult, - type LoadModelResponse, - type MeasureContext, - type OperationDomain, - type Ref, - type SearchOptions, - type SearchQuery, - type SearchResult, - type SessionData, - type Timestamp, - toFindResult, - type Tx -} from '@hcengineering/core' -import type { DomainResult } from '@hcengineering/core/src/storage' -import type { Middleware, PipelineContext, TxMiddlewareResult } from './types' - -export const emptyFindResult = Promise.resolve(toFindResult([])) -export const emptySearchResult = Promise.resolve({ docs: [], total: 0 }) -export const emptyTxResult = Promise.resolve({}) - -export const emptyBroadcastResult = Promise.resolve() -export const emptyModelResult = Promise.resolve([]) -/** - * @public - */ -export abstract class BaseMiddleware implements Middleware { - protected constructor ( - readonly context: PipelineContext, - protected readonly next?: Middleware - ) {} - - findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - return this.provideFindAll(ctx, _class, query, options) - } - - provideLoadModel ( - ctx: MeasureContext, - lastModelTx: Timestamp, - hash?: string - ): Promise { - return this.next?.loadModel(ctx, lastModelTx, hash) ?? emptyModelResult - } - - loadModel ( - ctx: MeasureContext, - lastModelTx: Timestamp, - hash?: string - ): Promise { - return this.provideLoadModel(ctx, lastModelTx, hash) - } - - provideGroupBy( - ctx: MeasureContext, - domain: Domain, - field: string, - query?: DocumentQuery

- ): Promise> { - if (this.next !== undefined) { - return this.next.groupBy(ctx, domain, field, query) - } - return Promise.resolve(new Map()) - } - - async close (): Promise {} - - groupBy( - ctx: MeasureContext, - domain: Domain, - field: string, - query?: DocumentQuery

- ): Promise> { - return this.provideGroupBy(ctx, domain, field, query) - } - - searchFulltext (ctx: MeasureContext, query: SearchQuery, options: SearchOptions): Promise { - return this.provideSearchFulltext(ctx, query, options) - } - - handleBroadcast (ctx: MeasureContext): Promise { - return this.next?.handleBroadcast(ctx) ?? emptyBroadcastResult - } - - provideBroadcast (ctx: MeasureContext): Promise { - return this.next?.handleBroadcast(ctx) ?? emptyBroadcastResult - } - - protected provideTx (ctx: MeasureContext, tx: Tx[]): Promise { - if (this.next !== undefined) { - return this.next.tx(ctx, tx) - } - return emptyTxResult - } - - tx (ctx: MeasureContext, tx: Tx[]): Promise { - return this.provideTx(ctx, tx) - } - - protected provideFindAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - if (this.next !== undefined) { - return this.next.findAll(ctx, _class, query, options) - } - return emptyFindResult - } - - protected provideSearchFulltext ( - ctx: MeasureContext, - query: SearchQuery, - options: SearchOptions - ): Promise { - if (this.next !== undefined) { - return this.next.searchFulltext(ctx, query, options) - } - return emptySearchResult - } - - domainRequest (ctx: MeasureContext, domain: OperationDomain, params: DomainParams): Promise { - return this.provideDomainRequest(ctx, domain, params) - } - - protected async provideDomainRequest ( - ctx: MeasureContext, - domain: OperationDomain, - params: DomainParams - ): Promise { - if (this.next !== undefined) { - return await this.next.domainRequest(ctx, domain, params) - } - return { domain, value: null } - } - - provideCloseSession (ctx: MeasureContext, sessionId: string): Promise { - if (this.next !== undefined) { - return this.next.closeSession(ctx, sessionId) - } - return Promise.resolve() - } - - closeSession (ctx: MeasureContext, sessionId: string): Promise { - return this.provideCloseSession(ctx, sessionId) - } -} diff --git a/server/core/src/benchmark/index.ts b/server/core/src/benchmark/index.ts deleted file mode 100644 index 6468b9a80a3..00000000000 --- a/server/core/src/benchmark/index.ts +++ /dev/null @@ -1,134 +0,0 @@ -// -// 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 core, { - generateId, - toFindResult, - TxProcessor, - type WorkspaceIds, - type BenchmarkDoc, - type Class, - type Doc, - type DocumentQuery, - type Domain, - type FindOptions, - type FindResult, - type Hierarchy, - type MeasureContext, - type ModelDb, - type Ref, - type Space, - type Tx, - type TxCreateDoc, - type TxResult -} from '@hcengineering/core' -import type { DbAdapter } from '../adapter' -import { DummyDbAdapter } from '../mem' - -function genData (dataSize: number): string { - let result = '' - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - const charactersLength = characters.length - for (let i = 0; i < dataSize; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)) - } - return result -} - -let benchData = '' - -class BenchmarkDbAdapter extends DummyDbAdapter { - async findAll>( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions | undefined - ): Promise> { - if (_class !== core.class.BenchmarkDoc) { - return toFindResult([]) - } - - if (benchData === '') { - benchData = genData(1024 * 1024) - } - - const result: BenchmarkDoc[] = [] - - const request: BenchmarkDoc['request'] = ((query as DocumentQuery) - .request as BenchmarkDoc['request']) ?? { - documents: 1, - size: 1 - } - const docsToAdd = - typeof request.documents === 'number' - ? request.documents - : request.documents.from + Math.random() * request.documents.to - for (let i = 0; i < docsToAdd; i++) { - const dataSize = - typeof request.size === 'number' ? request.size : request.size.from + Math.random() * request.size.to - result.push({ - _class: core.class.BenchmarkDoc, - _id: generateId(), - modifiedBy: core.account.System, - modifiedOn: Date.now(), - space: core.space.DerivedTx, // To be available for all - response: benchData.slice(0, dataSize) - }) - } - - return toFindResult(result as T[]) - } - - getDomainHash (ctx: MeasureContext, domain: Domain): Promise { - // Since benchmark coult not be changed. - return Promise.resolve('') - } - - tx (ctx: MeasureContext, ...tx: Tx[]): Promise { - if (benchData === '') { - benchData = genData(1024 * 1024) - } - for (const t of tx) { - if (t._class === core.class.TxCreateDoc) { - const doc = TxProcessor.createDoc2Doc(t as TxCreateDoc) - const request = doc.request - - if (request?.size != null) { - const dataSize = - typeof request.size === 'number' ? request.size : request.size.from + Math.random() * request.size.to - return Promise.resolve([ - { - response: benchData.slice(0, dataSize) - } - ]) - } - } - } - - return Promise.resolve([{}]) - } -} -/** - * @public - */ -export async function createBenchmarkAdapter ( - ctx: MeasureContext, - hierarchy: Hierarchy, - url: string, - workspaceId: WorkspaceIds, - modelDb: ModelDb -): Promise { - return new BenchmarkDbAdapter() -} diff --git a/server/core/src/configuration.ts b/server/core/src/configuration.ts deleted file mode 100644 index 71ee014568a..00000000000 --- a/server/core/src/configuration.ts +++ /dev/null @@ -1,47 +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 { type MeasureContext } from '@hcengineering/core' -import { type DbAdapterFactory } from './adapter' -import type { ContentTextAdapterFactory, ServiceAdapterConfig } from './types' - -/** - * @public - */ -export interface DbAdapterConfiguration { - factory: DbAdapterFactory - url: string -} - -/** - * @public - */ -export interface ContentTextAdapterConfiguration { - factory: ContentTextAdapterFactory - contentType: string - url: string -} - -/** - * @public - */ -export interface DbConfiguration { - adapters: Record - domains: Record - defaultAdapter: string - metrics: MeasureContext - serviceAdapters: Record -} diff --git a/server/core/src/content.ts b/server/core/src/content.ts deleted file mode 100644 index 059a557f892..00000000000 --- a/server/core/src/content.ts +++ /dev/null @@ -1,52 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type MeasureContext, type WorkspaceUuid } from '@hcengineering/core' -import { type ContentTextAdapterConfiguration } from './configuration' -import { type ContentTextAdapter } from './types' - -class ContentAdapter implements ContentTextAdapter { - constructor ( - private readonly adapters: Map, - private readonly defaultAdapter: ContentTextAdapter - ) {} - - content (ctx: MeasureContext, workspace: WorkspaceUuid, name: string, type: string, doc: Buffer): Promise { - const adapter = this.adapters.get(type) ?? this.defaultAdapter - return adapter.content(ctx, workspace, name, type, doc) - } -} - -export async function createContentAdapter ( - contentAdapters: Record, - defaultContentAdapter: string -): Promise { - const adapters = new Map() - let defaultAdapter: ContentTextAdapter | undefined - - for (const key in contentAdapters) { - const adapterConf = contentAdapters[key] - const adapter = await adapterConf.factory(adapterConf.url) - - adapters.set(adapterConf.contentType, adapter) - if (key === defaultContentAdapter) { - defaultAdapter = adapter - } - } - if (defaultAdapter === undefined) { - throw new Error('No default content adapter') - } - return new ContentAdapter(adapters, defaultAdapter) -} diff --git a/server/core/src/dbAdapterManager.ts b/server/core/src/dbAdapterManager.ts deleted file mode 100644 index 1f7b8153e99..00000000000 --- a/server/core/src/dbAdapterManager.ts +++ /dev/null @@ -1,228 +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 { Analytics } from '@hcengineering/analytics' -import { DOMAIN_TX, type Domain, type MeasureContext } from '@hcengineering/core' -import { type DbAdapter, type DomainHelper } from './adapter' -import type { DbConfiguration } from './configuration' -import { DummyDbAdapter } from './mem' -import type { DBAdapterManager, PipelineContext } from './types' - -interface DomainInfo { - exists: boolean - documents: number -} - -export class DbAdapterManagerImpl implements DBAdapterManager { - domainInfo = new Map() - - emptyAdapter = new DummyDbAdapter() - - domainHelper?: DomainHelper - - constructor ( - private readonly metrics: MeasureContext, - - readonly conf: DbConfiguration, - private readonly context: PipelineContext, - private readonly defaultAdapter: DbAdapter, - private readonly adapters: Map - ) {} - - reserveContext (id: string): () => void { - const ops: (() => void)[] = [] - for (const adapter of this.adapters.values()) { - try { - if (adapter.reserveContext !== undefined) { - ops.push(adapter.reserveContext(id)) - } - } catch (err: any) { - Analytics.handleError(err) - } - } - return () => { - for (const op of ops) { - op() - } - } - } - - getDefaultAdapter (): DbAdapter { - return this.defaultAdapter - } - - async registerHelper (ctx: MeasureContext, helper: DomainHelper): Promise { - this.domainHelper = helper - await this.initDomains(ctx) - } - - async initDomains (ctx: MeasureContext): Promise { - const adapterDomains = new Map>() - for (const d of this.context.hierarchy.domains()) { - // We need to init domain info - await ctx.with( - 'update-info', - { domain: d }, - async (ctx) => { - const info = this.getDomainInfo(d) - await this.updateInfo(d, adapterDomains, info) - }, - undefined, - { span: false } - ) - } - for (const [name, adapter] of this.adapters.entries()) { - await ctx.with( - 'domain-helper', - { name }, - async (ctx) => { - adapter.on?.((domain, event, count, helper) => { - const info = this.getDomainInfo(domain) - const oldDocuments = info.documents - switch (event) { - case 'add': - info.documents += count - break - case 'update': - break - case 'delete': - info.documents -= count - break - case 'read': - break - } - - if (oldDocuments < 50 && info.documents > 50) { - // We have more 50 documents, we need to check for indexes - void this.domainHelper?.checkDomain(this.metrics, domain, info.documents, helper) - } - if (oldDocuments > 50 && info.documents < 50) { - // We have more 50 documents, we need to check for indexes - void this.domainHelper?.checkDomain(this.metrics, domain, info.documents, helper) - } - }) - }, - undefined, - { span: false } - ) - } - } - - async initAdapters (ctx: MeasureContext): Promise { - for (const [key, adapter] of this.adapters) { - // already initialized - if (key !== this.conf.domains[DOMAIN_TX] && adapter.init !== undefined) { - let excludeDomains: string[] | undefined - let domains: string[] | undefined - if (this.conf.defaultAdapter === key) { - excludeDomains = [] - for (const domain in this.conf.domains) { - if (this.conf.domains[domain] !== key) { - excludeDomains.push(domain) - } - } - } else { - domains = [] - for (const domain in this.conf.domains) { - if (this.conf.domains[domain] === key) { - domains.push(domain) - } - } - } - await ctx.with(`init adapter ${key}`, {}, (ctx) => - adapter?.init?.(ctx, this.context.contextVars, domains, excludeDomains) - ) - } - } - } - - private async updateInfo (d: Domain, adapterDomains: Map>, info: DomainInfo): Promise { - const name = this.conf.domains[d] ?? '#default' - const adapter = this.adapters.get(name) ?? this.defaultAdapter - if (adapter !== undefined) { - const h = adapter.helper?.() - if (h !== undefined) { - const dbDomains = adapterDomains.get(adapter) ?? (await h.listDomains()) - adapterDomains.set(adapter, dbDomains) - info.exists = dbDomains.has(d) - if (info.exists) { - info.documents = await h.estimatedCount(d) - } - } else { - info.exists = true - } - } else { - info.exists = false - } - } - - private getDomainInfo (domain: Domain): DomainInfo { - let info = this.domainInfo.get(domain) - if (info === undefined) { - info = { - documents: 0, - exists: true - } - this.domainInfo.set(domain, info) - } - return info - } - - async close (): Promise { - for (const o of this.adapters.values()) { - try { - await o.close() - } catch (err: any) { - Analytics.handleError(err) - } - } - } - - getAdapterName (domain: Domain): string { - const adapterName = this.conf.domains[domain] - return adapterName ?? this.conf.defaultAdapter - } - - getAdapterByName (name: string): DbAdapter { - if (name === this.conf.defaultAdapter) { - return this.defaultAdapter - } - const adapter = this.adapters.get(name) ?? this.defaultAdapter - if (adapter === undefined) { - throw new Error('adapter not provided: ' + name) - } - - return adapter - } - - public getAdapter (domain: Domain, requireExists: boolean): DbAdapter { - const name = this.conf.domains[domain] ?? '#default' - const adapter = this.adapters.get(name) ?? this.defaultAdapter - if (adapter === undefined) { - throw new Error('adapter not provided: ' + name) - } - - const info = this.getDomainInfo(domain) - - if (!info.exists && !requireExists) { - return this.emptyAdapter - } - // If we require it exists, it will be exists - info.exists = true - - return adapter - } -} diff --git a/server/core/src/domainHelper.ts b/server/core/src/domainHelper.ts deleted file mode 100644 index adedf65d131..00000000000 --- a/server/core/src/domainHelper.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { Analytics } from '@hcengineering/analytics' -import type { - Doc, - Domain, - DomainIndexConfiguration, - FieldIndexConfig, - Hierarchy, - MeasureContext, - ModelDb, - WorkspaceUuid -} from '@hcengineering/core' -import core, { DOMAIN_BENCHMARK, DOMAIN_MODEL, IndexKind, IndexOrder } from '@hcengineering/core' -import { deepEqual } from 'fast-equals' -import type { DomainHelper, DomainHelperOperations } from './adapter' - -export class DomainIndexHelperImpl implements DomainHelper { - domains = new Map>>() - domainConfigurations: DomainIndexConfiguration[] = [] - constructor ( - readonly ctx: MeasureContext, - readonly hierarchy: Hierarchy, - readonly model: ModelDb, - readonly workspaceId: WorkspaceUuid - ) { - const classes = model.findAllSync(core.class.Class, {}) - - try { - this.domainConfigurations = - model.findAllSync(core.class.DomainIndexConfiguration, {}) ?? [] - } catch (err: any) { - this.domainConfigurations = [] - Analytics.handleError(err) - ctx.error('failed to find domain index configuration', { err }) - } - - this.domains = new Map>>() - // Find all domains and indexed fields inside - for (const c of classes) { - try { - const domain = hierarchy.findDomain(c._id) - if (domain === undefined || domain === DOMAIN_MODEL || domain === DOMAIN_BENCHMARK) { - continue - } - const attrs = hierarchy.getAllAttributes(c._id) - const domainAttrs = this.domains.get(domain) ?? new Set>() - for (const a of attrs.values()) { - if (a.isCustom === true) { - // Skip custom attribute indexes - continue - } - if (a.index !== undefined && a.index !== IndexKind.FullText) { - domainAttrs.add({ - keys: { - [a.name]: a.index === IndexKind.Indexed ? IndexOrder.Ascending : IndexOrder.Descending - }, - sparse: false // Default to non sparse indexes - }) - } - } - - // Handle extra configurations - if (hierarchy.hasMixin(c, core.mixin.IndexConfiguration)) { - const config = hierarchy.as(c, core.mixin.IndexConfiguration) - for (const attr of config.indexes) { - if (typeof attr === 'string') { - domainAttrs.add({ keys: { [attr]: IndexOrder.Ascending }, sparse: false }) - } else { - domainAttrs.add(attr) - } - } - } - - this.domains.set(domain, domainAttrs) - } catch (err: any) { - // Ignore, since we have classes without domain. - } - } - } - - /** - * Check if some indexes need to be created for domain. - */ - async checkDomain ( - ctx: MeasureContext, - domain: Domain, - documents: number, - operations: DomainHelperOperations - ): Promise { - const domainInfo = this.domains.get(domain) - const cfg = this.domainConfigurations.find((it) => it.domain === domain) - - const bb: (string | FieldIndexConfig)[] = [] - const added = new Set() - - try { - if (!(await operations.exists(domain))) { - return - } - const has50Documents = documents > 50 - const allIndexes = (await operations.listIndexes(domain)).filter((it) => it.name !== '_id_') - ctx.info('check indexes', { domain, has50Documents, documents }) - if (has50Documents) { - async function checkIndex (vv: string | FieldIndexConfig, checkDisabled: boolean): Promise { - try { - let name: string - if (typeof vv === 'string') { - name = `${vv}_sp_1` - } else { - let pfix = '' - if (vv.filter !== undefined) { - pfix += '_fi' - } else if (vv.sparse === true) { - pfix += '_sp' - } - name = Object.entries(vv.keys) - .map(([key, val]) => `${key + pfix}_${val}`) - .join('_') - } - - // Check if index is disabled or not - let isDisabled = - cfg?.disabled?.some((it) => { - const _it = typeof it === 'string' ? { [it]: 1 } : it - const _vv = typeof vv === 'string' ? { [vv]: 1 } : vv.keys - return deepEqual(_it, _vv) - }) ?? false - - if (!checkDisabled) { - isDisabled = false - } - if (isDisabled) { - // skip index since it is disabled - return - } - if (added.has(name)) { - // Index already added - return - } - added.add(name) - - const existingOne = allIndexes.findIndex((it) => it.name === name) - if (existingOne !== -1) { - allIndexes.splice(existingOne, 1) - } - const exists = existingOne !== -1 - // Check if index exists - if (!exists && !isDisabled) { - // Check if not disabled - bb.push(vv) - await operations.createIndex(domain, vv, { - name - }) - } - } catch (err: any) { - Analytics.handleError(err) - ctx.error('error: failed to create index', { domain, vv, err }) - } - } - - for (const vv of [...(domainInfo?.values() ?? [])]) { - await checkIndex(vv, true) - } - for (const vv of [...(cfg?.indexes ?? [])]) { - await checkIndex(vv, false) - } - } - if (allIndexes.length > 0) { - for (const c of allIndexes) { - try { - if (cfg?.skip !== undefined) { - if (Array.from(cfg.skip ?? []).some((it) => c.name.includes(it))) { - continue - } - } - ctx.info('drop index', { domain, name: c.name, has50Documents }) - await operations.dropIndex(domain, c.name) - } catch (err: any) { - Analytics.handleError(err) - console.error('error: failed to drop index', { c, err }) - } - } - } - } catch (err: any) { - ctx.error('error during domain collections/indexes check', { domain, error: err }) - Analytics.handleError(err) - } - - if (bb.length > 0) { - ctx.info('created indexes', { domain, bb }) - } - } -} diff --git a/server/core/src/index.ts b/server/core/src/index.ts deleted file mode 100644 index 8ca7cb27394..00000000000 --- a/server/core/src/index.ts +++ /dev/null @@ -1,37 +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. -// - -export type { StorageAdapter } from '@hcengineering/storage' -export * from './adapter' -export * from './base' -export * from './benchmark' -export * from './configuration' -export * from './limitter' -export * from './mem' -export * from './pipeline' -export { default, serverCoreId } from './plugin' -export * from './storage' -export * from './types' -export * from './utils' - -export * from './content' -export * from './dbAdapterManager' -export * from './domainHelper' -export * from './nullAdapter' -export * from './service' -export * from './stats' -export * from './triggers' -export * from './queue' diff --git a/server/core/src/limitter.ts b/server/core/src/limitter.ts deleted file mode 100644 index a2c34051223..00000000000 --- a/server/core/src/limitter.ts +++ /dev/null @@ -1 +0,0 @@ -export { RateLimiter } from '@hcengineering/core' diff --git a/server/core/src/mem.ts b/server/core/src/mem.ts deleted file mode 100644 index a8a75429792..00000000000 --- a/server/core/src/mem.ts +++ /dev/null @@ -1,189 +0,0 @@ -// -// 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 core, { - type Class, - type Doc, - type DocumentQuery, - type DocumentUpdate, - type Domain, - type FindOptions, - type FindResult, - type Hierarchy, - type IndexingConfiguration, - type Iterator, - type MeasureContext, - ModelDb, - type Ref, - type StorageIterator, - toFindResult, - type Tx, - type TxCUD, - TxProcessor, - type TxResult, - type WorkspaceIds -} from '@hcengineering/core' -import { type DbAdapter, type DbAdapterHandler, type DomainHelperOperations, type RawFindIterator } from './adapter' -/** - * @public - */ -export class DummyDbAdapter implements DbAdapter { - on?: ((handler: DbAdapterHandler) => void) | undefined - - async traverse( - domain: Domain, - query: DocumentQuery, - options?: Pick, 'sort' | 'limit' | 'projection'> - ): Promise> { - return { - next: async () => [], - close: async () => {} - } - } - - async findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions | undefined - ): Promise> { - return toFindResult([]) - } - - rawFind (ctx: MeasureContext, domain: Domain): RawFindIterator { - return { - find: async () => [], - close: async () => {} - } - } - - async init (): Promise {} - - helper (): DomainHelperOperations { - return { - create: async () => {}, - exists: async () => true, - listDomains: async () => new Set(), - createIndex: async () => {}, - dropIndex: async () => {}, - listIndexes: async () => [], - estimatedCount: async () => 0 - } - } - - async createIndexes (domain: Domain, config: Pick, 'indexes'>): Promise {} - async removeOldIndex (domain: Domain, deletePattern: RegExp[], keepPattern: RegExp[]): Promise {} - - async tx (ctx: MeasureContext, ...tx: Tx[]): Promise { - return [] - } - - async close (): Promise {} - - find (ctx: MeasureContext, domain: Domain): StorageIterator { - return { - next: async () => [], - close: async () => {} - } - } - - async load (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return [] - } - - async upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise {} - - async clean (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise {} - - getDomainHash (ctx: MeasureContext, domain: Domain): Promise { - // Return '' for empty documents content. - return Promise.resolve('') - } - - async groupBy( - ctx: MeasureContext, - domain: Domain, - field: string, - query?: DocumentQuery

- ): Promise> { - return new Map() - } - - async rawFindAll(domain: Domain, query: DocumentQuery, options?: FindOptions): Promise { - return [] - } - - async rawUpdate( - domain: Domain, - query: DocumentQuery, - operations: DocumentUpdate - ): Promise {} - - async rawDeleteMany(domain: Domain, query: DocumentQuery): Promise {} -} - -class InMemoryAdapter extends DummyDbAdapter implements DbAdapter { - private readonly modeldb: ModelDb - - constructor (readonly hierarchy: Hierarchy) { - super() - this.modeldb = new ModelDb(hierarchy) - } - - findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - return ctx.withSync('inmem-find', {}, () => this.modeldb.findAll(_class, query, options)) - } - - load (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return this.modeldb.findAll(core.class.Doc, { _id: { $in: docs } }) - } - - tx (ctx: MeasureContext, ...tx: Tx[]): Promise { - // Filter transactions with broadcast only flags - const ftx = tx.filter((it) => { - if (TxProcessor.isExtendsCUD(it._class)) { - const cud = it as TxCUD - const objClass = this.hierarchy.getClass(cud.objectClass) - const mix = this.hierarchy.hasMixin(objClass, core.mixin.TransientConfiguration) - if (mix && this.hierarchy.as(objClass, core.mixin.TransientConfiguration).broadcastOnly) { - // We do not need to store a broadcast only transactions into model. - return false - } - } - return true - }) - if (ftx.length === 0) { - return Promise.resolve([]) - } - return this.modeldb.tx(...ftx) - } -} - -/** - * @public - */ -export async function createInMemoryAdapter ( - ctx: MeasureContext, - hierarchy: Hierarchy, - url: string, - workspaceId: WorkspaceIds -): Promise { - return new InMemoryAdapter(hierarchy) -} diff --git a/server/core/src/nullAdapter.ts b/server/core/src/nullAdapter.ts deleted file mode 100644 index 9c0d3adb7e2..00000000000 --- a/server/core/src/nullAdapter.ts +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright © 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type WorkspaceIds, type Hierarchy, type MeasureContext, type ModelDb } from '@hcengineering/core' -import type { DbAdapter } from './adapter' -import { DummyDbAdapter } from './mem' - -/** - * @public - */ -export async function createNullAdapter ( - ctx: MeasureContext, - hierarchy: Hierarchy, - url: string, - workspaceId: WorkspaceIds, - modelDb: ModelDb -): Promise { - return new DummyDbAdapter() -} diff --git a/server/core/src/pipeline.ts b/server/core/src/pipeline.ts deleted file mode 100644 index f71cbc101c4..00000000000 --- a/server/core/src/pipeline.ts +++ /dev/null @@ -1,155 +0,0 @@ -// -// 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 { Analytics } from '@hcengineering/analytics' -import { - toFindResult, - withContext, - type Class, - type Doc, - type DocumentQuery, - type Domain, - type DomainParams, - type DomainResult, - type FindOptions, - type FindResult, - type LoadModelResponse, - type MeasureContext, - type OperationDomain, - type Ref, - type SearchOptions, - type SearchQuery, - type SearchResult, - type SessionData, - type Timestamp, - type Tx, - type TxResult -} from '@hcengineering/core' -import { emptyBroadcastResult } from './base' -import { type Middleware, type MiddlewareCreator, type Pipeline, type PipelineContext } from './types' - -/** - * @public - */ -export async function createPipeline ( - ctx: MeasureContext, - constructors: MiddlewareCreator[], - context: PipelineContext -): Promise { - return await PipelineImpl.create(ctx, constructors, context) -} - -class PipelineImpl implements Pipeline { - private head: Middleware | undefined - - private readonly middlewares: Middleware[] = [] - - private constructor (readonly context: PipelineContext) {} - - static async create ( - ctx: MeasureContext, - constructors: MiddlewareCreator[], - context: PipelineContext - ): Promise { - const pipeline = new PipelineImpl(context) - - pipeline.head = await pipeline.buildChain(ctx, constructors, pipeline.context) - context.head = pipeline.head - return pipeline - } - - @withContext('build-chain') - private async buildChain ( - ctx: MeasureContext, - constructors: MiddlewareCreator[], - context: PipelineContext - ): Promise { - let current: Middleware | undefined - for (let index = constructors.length - 1; index >= 0; index--) { - const element = constructors[index] - try { - const newCur = await element(ctx, context, current) - if (newCur != null) { - this.middlewares.push(newCur) - } - current = newCur ?? current - } catch (err: any) { - ctx.error('failed to initialize pipeline', { err, workspace: context.workspace }) - // We need to call close for all items. - await this.close() - throw err - } - } - this.middlewares.reverse() - - return current - } - - findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - return this.head?.findAll(ctx, _class, query, options) ?? Promise.resolve(toFindResult([])) - } - - loadModel (ctx: MeasureContext, lastModelTx: Timestamp, hash?: string): Promise { - return this.head?.loadModel(ctx, lastModelTx, hash) ?? Promise.resolve([]) - } - - groupBy( - ctx: MeasureContext, - domain: Domain, - field: string, - query?: DocumentQuery

- ): Promise> { - return this.head?.groupBy(ctx, domain, field, query) ?? Promise.resolve(new Map()) - } - - searchFulltext (ctx: MeasureContext, query: SearchQuery, options: SearchOptions): Promise { - return this.head?.searchFulltext(ctx, query, options) ?? Promise.resolve({ docs: [] }) - } - - tx (ctx: MeasureContext, tx: Tx[]): Promise { - return this.head?.tx(ctx, tx) ?? Promise.resolve({}) - } - - handleBroadcast (ctx: MeasureContext): Promise { - return this.head?.handleBroadcast(ctx) ?? emptyBroadcastResult - } - - domainRequest( - ctx: MeasureContext, - domain: OperationDomain, - params: DomainParams - ): Promise> { - return this.head?.domainRequest(ctx, domain, params) ?? Promise.resolve({ domain, value: undefined }) - } - - closeSession (ctx: MeasureContext, sessionId: string): Promise { - return this.head?.closeSession(ctx, sessionId) ?? Promise.resolve() - } - - async close (): Promise { - for (const mw of this.middlewares) { - try { - await mw.close() - } catch (err: any) { - Analytics.handleError(err) - } - } - } -} diff --git a/server/core/src/plugin.ts b/server/core/src/plugin.ts deleted file mode 100644 index e1fea8facf2..00000000000 --- a/server/core/src/plugin.ts +++ /dev/null @@ -1,46 +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 { type Metadata, type Plugin, plugin } from '@hcengineering/platform' - -import type { Class, Mixin, Ref } from '@hcengineering/core' -import type { ObjectDDParticipant, SearchPresenter, Trigger } from './types' - -/** - * @public - */ -export const serverCoreId = 'server-core' as Plugin - -/** - * @public - */ -const serverCore = plugin(serverCoreId, { - class: { - Trigger: '' as Ref> - }, - mixin: { - ObjectDDParticipant: '' as Ref, - SearchPresenter: '' as Ref> - }, - metadata: { - FrontUrl: '' as Metadata, - FilesUrl: '' as Metadata, - ElasticIndexName: '' as Metadata, - ElasticIndexVersion: '' as Metadata - } -}) - -export default serverCore diff --git a/server/core/src/queue/dummyQueue.ts b/server/core/src/queue/dummyQueue.ts deleted file mode 100644 index b4c1d9e9ce6..00000000000 --- a/server/core/src/queue/dummyQueue.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { MeasureContext, WorkspaceUuid } from '@hcengineering/core' -import { type ConsumerHandle, type PlatformQueue, type PlatformQueueProducer, type QueueTopic } from './types' - -/** - * A dummy implementation of PlatformQueueProducer for testing and development - */ -class DummyQueueProducer implements PlatformQueueProducer { - async send (ctx: MeasureContext, id: WorkspaceUuid | string, msgs: T[]): Promise { - await Promise.resolve() - } - - async close (): Promise { - await Promise.resolve() - } - - getQueue (): PlatformQueue { - return new DummyQueue() - } -} - -/** - * A dummy implementation of PlatformQueue for testing and development - */ -export class DummyQueue implements PlatformQueue { - getProducer(ctx: MeasureContext, topic: QueueTopic | string): PlatformQueueProducer { - return new DummyQueueProducer() - } - - getClientId (): string { - return 'dummy' - } - - async shutdown (): Promise {} - - async createTopic (topics: string | string[], partitions: number): Promise {} - - createConsumer( - ctx: MeasureContext, - topic: QueueTopic | string, - groupId: string, - onMessage: ( - ctx: MeasureContext, - msg: { workspace: WorkspaceUuid, value: T }, - queue: { - pause: () => void - heartbeat: () => Promise - } - ) => Promise, - options?: { - bulkSize?: number - fromBegining?: boolean - } - ): ConsumerHandle { - return { - close: async (): Promise => { - await Promise.resolve() - }, - isConnected: (): boolean => { - return false - } - } - } - - async createTopics (tx: number): Promise { - await Promise.resolve() - } - - async deleteTopics (topics?: (QueueTopic | string)[]): Promise {} -} - -/** - * Creates a new instance of the dummy queue implementation - */ -export function createDummyQueue (): PlatformQueue { - return new DummyQueue() -} diff --git a/server/core/src/queue/index.ts b/server/core/src/queue/index.ts deleted file mode 100644 index 9121f8d22a6..00000000000 --- a/server/core/src/queue/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './types' -export * from './workspace' -export * from './dummyQueue' -export * from './users' -export * from './utils' diff --git a/server/core/src/queue/types.ts b/server/core/src/queue/types.ts deleted file mode 100644 index 618e5511bc3..00000000000 --- a/server/core/src/queue/types.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { MeasureContext, WorkspaceUuid } from '@hcengineering/core' - -export enum QueueTopic { - // Topic with partitions to split workspace transactions into - Tx = 'tx', - - // Topic to send workspace information into - Workspace = 'workspace', - - // A fulltext topic to hold extra fulltext request, like re-indexing requests etc, private to fulltext service. - Fulltext = 'fulltext', - - // A topic about user activity. - Users = 'users', - - TelegramBot = 'telegramBot', - - // A topic about calendar events. - CalendarEventCUD = 'calendarEventCUD', - - // A topic about process events - Process = 'process' -} - -export interface ConsumerHandle { - close: () => Promise - isConnected: () => boolean -} - -export interface ConsumerMessage { - workspace: WorkspaceUuid - value: T -} - -export interface ConsumerControl { - pause: () => void - heartbeat: () => Promise -} - -export interface PlatformQueue { - getProducer: (ctx: MeasureContext, topic: QueueTopic | string) => PlatformQueueProducer - - /** - * Create a consumer for a topic. - * return a function to close the reciever. - */ - createConsumer: ( - ctx: MeasureContext, - topic: QueueTopic | string, - groupId: string, - onMessage: (ctx: MeasureContext, msg: ConsumerMessage, queue: ConsumerControl) => Promise, - options?: { - fromBegining?: boolean - } - ) => ConsumerHandle - - createTopic: (topics: string | string[], partitions: number) => Promise - - createTopics: (tx: number) => Promise - - // If not passed will delete all topica from QueueTopic enum - deleteTopics: (topics?: (QueueTopic | string)[]) => Promise - - getClientId: () => string - - // Will close all producers and consumers - shutdown: () => Promise -} - -/** - * Create a producer for a topic. - */ -export interface PlatformQueueProducer { - send: (ctx: MeasureContext, workspace: WorkspaceUuid, msgs: T[], partitionKey?: string) => Promise - close: () => Promise - - getQueue: () => PlatformQueue -} diff --git a/server/core/src/queue/users.ts b/server/core/src/queue/users.ts deleted file mode 100644 index 40c25970f0c..00000000000 --- a/server/core/src/queue/users.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { AccountUuid, PersonId } from '@hcengineering/core' - -export enum QueueUserEvent { - login = 'login', - logout = 'logout' -} - -export interface QueueUserMessage { - type: QueueUserEvent - user: AccountUuid -} - -export interface QueueUserLogin extends QueueUserMessage { - type: QueueUserEvent.login | QueueUserEvent.logout - sessions: number - socialIds: PersonId[] -} - -export const userEvents = { - login: function userLogin (data: Omit): QueueUserLogin { - return { - type: QueueUserEvent.login, - ...data - } - }, - logout: function userLogout (data: Omit): QueueUserLogin { - return { - type: QueueUserEvent.logout, - ...data - } - } -} diff --git a/server/core/src/queue/utils.ts b/server/core/src/queue/utils.ts deleted file mode 100644 index 647d618ffe3..00000000000 --- a/server/core/src/queue/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { type QueueTopic } from './types' - -export function getDeadletterTopic (topic: QueueTopic): string { - return `${topic}-d` -} diff --git a/server/core/src/queue/workspace.ts b/server/core/src/queue/workspace.ts deleted file mode 100644 index 2717bf5d97a..00000000000 --- a/server/core/src/queue/workspace.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Class, Doc, Domain, Ref } from '@hcengineering/core' - -export enum QueueWorkspaceEvent { - Up = 'up', - Down = 'down', - Created = 'created', - CreateFailed = 'create-failed', - Upgraded = 'upgraded', - Upgradefailed = 'upgrade-failed', - Deleted = 'deleted', - Archived = 'archived', - Restored = 'restored', - Restoring = 'restoring', - FullReindex = 'full-fulltext-reindex', - Reindex = 'fulltext-reindex', - ClearIndex = 'clear-fulltext-index' -} - -export interface QueueWorkspaceMessage { - type: QueueWorkspaceEvent -} - -export interface QueueWorkspaceReindexMessage extends QueueWorkspaceMessage { - type: QueueWorkspaceEvent.Reindex - - domain: Domain - classes: Ref>[] -} - -export const workspaceEvents = { - open: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.Up }), - down: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.Down }), - created: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.Created }), - upgraded: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.Upgraded }), - upgradeFailed: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.Upgradefailed }), - createFailed: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.CreateFailed }), - deleted: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.Deleted }), - archived: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.Archived }), - restoring: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.Restoring }), - restored: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.Restored }), - fullReindex: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.FullReindex }), - clearIndex: (): QueueWorkspaceMessage => ({ type: QueueWorkspaceEvent.ClearIndex }), - reindex: (domain: Domain, classes: Ref>[]): QueueWorkspaceReindexMessage => ({ - type: QueueWorkspaceEvent.Reindex, - domain, - classes - }) -} diff --git a/server/core/src/service.ts b/server/core/src/service.ts deleted file mode 100644 index feec6b7506f..00000000000 --- a/server/core/src/service.ts +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type MeasureContext } from '@hcengineering/core' - -import { type ServiceAdapter, type ServiceAdapterConfig, type ServiceAdaptersManager } from './types' - -export class ServiceAdaptersManagerImpl implements ServiceAdaptersManager { - constructor ( - private readonly adapters: Map, - private readonly context: MeasureContext - ) {} - - getAdapter (adapterId: string): ServiceAdapter | undefined { - return this.adapters.get(adapterId) - } - - async close (): Promise { - for (const adapter of this.adapters.values()) { - await adapter.close() - } - } - - metrics (): MeasureContext { - return this.context - } -} - -export async function createServiceAdaptersManager ( - serviceAdapters: Record, - context: MeasureContext -): Promise { - const adapters = new Map() - - for (const key in serviceAdapters) { - const adapterConf = serviceAdapters[key] - const adapter = await adapterConf.factory(adapterConf.url, adapterConf.db, context.newChild(key, {})) - - adapters.set(key, adapter) - } - return new ServiceAdaptersManagerImpl(adapters, context) -} diff --git a/server/core/src/stats.ts b/server/core/src/stats.ts deleted file mode 100644 index 4cfac0b1846..00000000000 --- a/server/core/src/stats.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { MeasureContext, Metrics } from '@hcengineering/core' -import { concatLink, MeasureMetricsContext, newMetrics, systemAccountUuid } from '@hcengineering/core' -import { generateToken } from '@hcengineering/server-token' -import os from 'os' - -export interface MemoryStatistics { - memoryUsed: number - memoryTotal: number - - memoryArrayBuffers: number - memoryRSS: number - freeMem: number - totalMem: number -} -export interface CPUStatistics { - usage: number - cores: number -} - -/** - * @public - */ -export interface StatisticsElement { - find: number - tx: number -} - -export interface UserStatistics { - userId: string - sessionId: string - data: any - mins5: StatisticsElement - total: StatisticsElement - current: StatisticsElement -} - -export interface WorkspaceStatistics { - sessions: UserStatistics[] - workspaceName: string - wsId: string - sessionsTotal: number - clientsTotal: number - - service?: string -} -export interface ServiceStatistics { - serviceName: string // A service category - memory: MemoryStatistics - cpu: CPUStatistics - stats?: Metrics - workspaces?: WorkspaceStatistics[] -} - -export function getMemoryInfo (): MemoryStatistics { - const memU = process.memoryUsage() - return { - memoryUsed: Math.round((memU.heapUsed / 1024 / 1024) * 100) / 100, - memoryRSS: Math.round((memU.rss / 1024 / 1024) * 100) / 100, - memoryTotal: Math.round((memU.heapTotal / 1024 / 1024) * 100) / 100, - memoryArrayBuffers: Math.round((memU.arrayBuffers / 1024 / 1024) * 100) / 100, - freeMem: Math.round((os.freemem() / 1024 / 1024) * 100) / 100, - totalMem: Math.round((os.totalmem() / 1024 / 1024) * 100) / 100 - } -} - -export function getCPUInfo (): CPUStatistics { - return { - usage: Math.round(os.loadavg()[0] * 100) / 100, - cores: os.cpus().length - } -} - -const METRICS_UPDATE_INTERVAL = 5000 -/** - * @public - */ -export function initStatisticsContext ( - serviceName: string, - ops?: { - logFile?: string - factory?: () => MeasureContext - getStats?: () => WorkspaceStatistics[] - statsUrl?: string - serviceName?: () => string - } -): MeasureContext { - let metricsContext: MeasureContext - if (ops?.factory !== undefined) { - metricsContext = ops.factory() - } else { - metricsContext = new MeasureMetricsContext(serviceName, {}, {}, newMetrics()) - } - - const statsUrl = ops?.statsUrl ?? process.env.STATS_URL - - let errorToSend = 0 - - if (statsUrl !== undefined) { - metricsContext.info('using stats url', { statsUrl, service: serviceName ?? '' }) - const serviceId = encodeURIComponent(os.hostname() + '-' + serviceName) - - let prev: Promise | Promise | undefined - const handleError = (err: any): void => { - errorToSend++ - if (errorToSend % 2 === 0) { - if (err.code !== 'UND_ERR_SOCKET') { - console.error(err) - } - } - prev = undefined - } - - const intTimer = setInterval(() => { - try { - if (prev !== undefined) { - // In case of high load, skip - return - } - if (statsUrl !== undefined) { - const token = generateToken(systemAccountUuid, undefined, { service: serviceName }) - const data: ServiceStatistics = { - serviceName: ops?.serviceName?.() ?? serviceName, - cpu: getCPUInfo(), - memory: getMemoryInfo(), - stats: metricsContext.metrics, - workspaces: ops?.getStats?.() - } - - const statData = JSON.stringify(data) - - void metricsContext.with( - 'sendStatistics', - {}, - async (ctx) => { - prev = fetch(concatLink(statsUrl, '/api/v1/statistics') + `/?name=${serviceId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - authorization: `Bearer ${token}` - }, - body: statData - }) - .finally(() => { - prev = undefined - }) - .catch(handleError) - }, - undefined, - { span: 'disable' } - ) - } - } catch (err: any) { - handleError(err) - } - }, METRICS_UPDATE_INTERVAL) - - const closeTimer = (): void => { - clearInterval(intTimer) - } - process.on('SIGINT', closeTimer) - process.on('SIGTERM', closeTimer) - } - - return metricsContext -} diff --git a/server/core/src/storage.ts b/server/core/src/storage.ts deleted file mode 100644 index 05182ec50f1..00000000000 --- a/server/core/src/storage.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { - type Doc, - type DocInfo, - type Domain, - type LowLevelStorage, - type MeasureContext, - type Ref, - type StorageIterator, - type WorkspaceUuid -} from '@hcengineering/core' - -export * from '@hcengineering/storage' - -/** - * @public - */ -export function getBucketId (workspace: WorkspaceUuid): string { - return workspace -} - -const chunkSize = 50000 - -/** - * @public - */ -export interface ChunkInfo { - idx: number - index: 0 - finished: boolean - iterator: StorageIterator -} - -/** - * @public - */ -export class BackupClientOps { - constructor (protected readonly storage: LowLevelStorage) {} - - idIndex = 0 - chunkInfo = new Map() - - loadChunk ( - ctx: MeasureContext, - domain: Domain, - idx?: number - ): Promise<{ - idx: number - size?: number - docs: DocInfo[] - finished: boolean - }> { - return ctx.with('load-chunk', {}, async (ctx) => { - idx = idx ?? this.idIndex++ - let chunk: ChunkInfo | undefined = this.chunkInfo.get(idx) - let size = 0 - if (chunk !== undefined) { - chunk.index++ - if (chunk.finished === undefined || chunk.finished) { - return { - idx, - docs: [], - finished: true - } - } - } else { - chunk = { idx, iterator: this.storage.find(ctx, domain), finished: false, index: 0 } - this.chunkInfo.set(idx, chunk) - } - const docs: DocInfo[] = [] - - while (docs.length < chunkSize) { - const _docs = await chunk.iterator.next(ctx) - if (_docs.length === 0) { - chunk.finished = true - break - } - docs.push(..._docs) - for (const d of _docs) { - size += d.size ?? 0 - } - } - - return { - idx, - docs, - finished: chunk.finished, - size - } - }) - } - - getDomainHash (ctx: MeasureContext, domain: Domain): Promise { - return this.storage.getDomainHash(ctx, domain) - } - - closeChunk (ctx: MeasureContext, idx: number): Promise { - return ctx.with('close-chunk', {}, async () => { - const chunk = this.chunkInfo.get(idx) - this.chunkInfo.delete(idx) - if (chunk != null) { - await chunk.iterator.close(ctx) - } - }) - } - - loadDocs (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return this.storage.load(ctx, domain, docs) - } - - upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise { - return this.storage.upload(ctx, domain, docs) - } - - clean (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return this.storage.clean(ctx, domain, docs) - } -} diff --git a/server/core/src/triggers.ts b/server/core/src/triggers.ts deleted file mode 100644 index bba7f9f2a1b..00000000000 --- a/server/core/src/triggers.ts +++ /dev/null @@ -1,197 +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 core, { - TxFactory, - TxProcessor, - groupByArray, - matchQuery, - type Class, - type Doc, - type DocumentQuery, - type Hierarchy, - type MeasureContext, - type ModelDb, - type Obj, - type Ref, - type Tx, - type TxCreateDoc, - type WithLookup -} from '@hcengineering/core' - -import { Analytics } from '@hcengineering/analytics' -import { getResource, type Resource } from '@hcengineering/platform' -import type { Trigger, TriggerControl, TriggerFunc } from './types' - -import serverCore from './plugin' - -interface TriggerRecord { - query?: DocumentQuery - trigger: { op: TriggerFunc | Promise, resource: Resource, isAsync: boolean } -} -/** - * @public - */ -export class Triggers { - private readonly triggers: TriggerRecord[] = [] - - constructor (protected readonly hierarchy: Hierarchy) {} - - init (model: ModelDb): void { - const allTriggers = model.findAllSync(serverCore.class.Trigger, {}) - for (const t of allTriggers) { - this.addTrigger(t) - } - } - - private addTrigger (t: WithLookup): void { - const match = t.txMatch - - const trigger = t.trigger - const func = getResource(trigger) - const isAsync = t.isAsync === true - this.triggers.push({ - query: match, - trigger: { op: func, resource: trigger, isAsync } - }) - } - - tresolve = Promise.resolve() - tx (txes: Tx[]): Promise { - for (const tx of txes) { - if (tx._class === core.class.TxCreateDoc) { - const createTx = tx as TxCreateDoc - if (createTx.objectClass === serverCore.class.Trigger) { - const trigger = TxProcessor.createDoc2Doc(createTx as TxCreateDoc) - this.addTrigger(trigger) - } - } - } - return this.tresolve - } - - async applyTrigger ( - ctx: MeasureContext, - ctrl: Omit, - matches: Tx[], - { trigger }: TriggerRecord - ): Promise { - const result: Tx[] = [] - const apply: Tx[] = [] - const group = groupByArray(matches, (it) => it.modifiedBy) - - const tctrl: TriggerControl = { - ...ctrl, - ctx, - txFactory: null as any, // Will be set later - apply: (ctx, tx, needResult) => { - if (needResult !== true) { - apply.push(...tx) - } - ctrl.txes.push(...tx) // We need to put them so other triggers could check if similar operation is already performed. - return ctrl.apply(ctx, tx, needResult) - } - } - if (trigger.op instanceof Promise) { - trigger.op = await trigger.op - } - for (const [k, v] of group.entries()) { - tctrl.txFactory = new TxFactory(k, true) - try { - const tresult = await trigger.op(v, tctrl) - result.push(...tresult) - ctrl.txes.push(...tresult) - } catch (err: any) { - ctx.error('failed to process trigger', { trigger: trigger.resource, err }) - Analytics.handleError(err) - } - } - return result.concat(apply) - } - - async apply ( - ctx: MeasureContext, - tx: Tx[], - ctrl: Omit, - mode: 'sync' | 'async' - ): Promise { - const result: Tx[] = [] - for (const { query, trigger } of this.triggers) { - if ((trigger.isAsync ? 'async' : 'sync') !== mode) { - continue - } - let matches = tx - if (query !== undefined) { - this.addDerived(query, 'objectClass') - this.addDerived(query, 'attachedToClass') - matches = matchQuery(tx, query, core.class.Tx, ctrl.hierarchy) as Tx[] - } - if (matches.length > 0) { - await ctx.with( - trigger.resource, - {}, - async (ctx) => { - try { - const tresult = await this.applyTrigger(ctx, ctrl, matches, { trigger }) - result.push(...tresult) - } catch (err: any) { - ctx.error('error during async processing', { err }) - } - }, - { count: matches.length, workspace: ctrl.workspace.uuid } - ) - } - } - - return result - } - - private addDerived (q: DocumentQuery, key: string): void { - if (q[key] === undefined) { - return - } - if (typeof q[key] === 'string') { - const descendants = this.hierarchy.getDescendants(q[key] as Ref>) - q[key] = { - $in: [q[key] as Ref>, ...descendants] - } - } else { - if (Array.isArray(q[key].$in)) { - const oldIn = q[key].$in - const newIn = new Set(oldIn) - q[key].$in.forEach((element: Ref>) => { - const descendants = this.hierarchy.getDescendants(element) - descendants.forEach((d) => newIn.add(d)) - }) - q[key].$in = Array.from(newIn.values()) - } - if (Array.isArray(q[key].$nin)) { - const oldNin = q[key].$nin - const newNin = new Set(oldNin) - q[key].$nin.forEach((element: Ref>) => { - const descendants = this.hierarchy.getDescendants(element) - descendants.forEach((d) => newNin.add(d)) - }) - q[key].$nin = Array.from(newNin.values()) - } - if (q[key].$ne !== undefined) { - const descendants = this.hierarchy.getDescendants(q[key].$ne) - delete q[key].$ne - q[key].$nin = [...(q[key].$nin ?? []), ...descendants] - } - } - } -} diff --git a/server/core/src/types.ts b/server/core/src/types.ts deleted file mode 100644 index c48c3d4aa0f..00000000000 --- a/server/core/src/types.ts +++ /dev/null @@ -1,780 +0,0 @@ -// -// Copyright © 2022, 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type ServerApi as CommunicationApi } from '@hcengineering/communication-sdk-types' -import { - type Account, - type AccountRole, - type AccountUuid, - type Branding, - type Class, - type Doc, - type DocumentQuery, - type Domain, - type DomainParams, - type DomainResult, - type FindOptions, - type FindResult, - type Hierarchy, - type LoadModelResponse, - type LowLevelStorage, - type MeasureContext, - type ModelDb, - type Obj, - type OperationDomain, - type PersonId, - type Ref, - type SearchOptions, - type SearchQuery, - type SearchResult, - type SessionData, - type SocialId, - type Space, - type Timestamp, - type Tx, - type TxFactory, - type TxResult, - type UserStatus, - type WorkspaceIds, - type WorkspaceUuid -} from '@hcengineering/core' -import type { Asset, Resource } from '@hcengineering/platform' -import type { LiveQuery } from '@hcengineering/query' -import type { RateLimitInfo, ReqId, Request, Response } from '@hcengineering/rpc' -import type { Token } from '@hcengineering/server-token' - -import type { DbAdapter, DomainHelper } from './adapter' -import { type PlatformQueue, type PlatformQueueProducer, type QueueTopic } from './queue' -import type { StatisticsElement, WorkspaceStatistics } from './stats' -import { type StorageAdapter } from './storage' - -export interface ServerFindOptions extends FindOptions { - prefix?: string - - skipClass?: boolean - skipSpace?: boolean - - // using for join query security - allowedSpaces?: Ref[] - - // Optional measure context, for server side operations - ctx?: MeasureContext -} - -export type SessionFindAll = ( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: ServerFindOptions -) => Promise> - -/** - * @public - */ -export interface Middleware { - findAll: ( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: ServerFindOptions - ) => Promise> - tx: (ctx: MeasureContext, tx: Tx[]) => Promise - - groupBy: ( - ctx: MeasureContext, - domain: Domain, - field: string, - query?: DocumentQuery

- ) => Promise> - searchFulltext: ( - ctx: MeasureContext, - query: SearchQuery, - options: SearchOptions - ) => Promise - - handleBroadcast: (ctx: MeasureContext) => Promise - - loadModel: ( - ctx: MeasureContext, - lastModelTx: Timestamp, - hash?: string - ) => Promise - - // Operation to some specific domain - domainRequest: ( - ctx: MeasureContext, - domain: OperationDomain, - params: DomainParams - ) => Promise - closeSession: (ctx: MeasureContext, sessionId: string) => Promise - close: () => Promise -} - -/** - * @public - */ -export interface BroadcastOps { - broadcast: (ctx: MeasureContext, tx: Tx[], targets?: AccountUuid | AccountUuid[], exclude?: AccountUuid[]) => void - - broadcastSessions: (measure: MeasureContext, sessionIds: Record) => void -} - -export interface CommunicationCallbacks { - registerAsyncRequest: (ctx: MeasureContext, promise: (ctx: MeasureContext) => Promise) => void - broadcast: (ctx: MeasureContext, sessionIds: Record) => void - enqueue: (ctx: MeasureContext, result: any[]) => void -} - -/** - * @public - */ -export type MiddlewareCreator = ( - ctx: MeasureContext, - context: PipelineContext, - next?: Middleware -) => Promise - -export interface ServiceAdaptersManager { - getAdapter: (adapterId: string) => ServiceAdapter | undefined - close: () => Promise - metrics: () => MeasureContext -} - -/** - * @public - */ -export type TxMiddlewareResult = TxResult - -export interface DBAdapterManager { - getAdapter: (domain: Domain, requireExists: boolean) => DbAdapter - - getAdapterName: (domain: Domain) => string - getAdapterByName: (name: string, requireExists: boolean) => DbAdapter - - getDefaultAdapter: () => DbAdapter - - close: () => Promise - - registerHelper: (ctx: MeasureContext, helper: DomainHelper) => Promise - - initAdapters: (ctx: MeasureContext) => Promise - - reserveContext: (id: string) => () => void - - domainHelper?: DomainHelper -} - -export interface PipelineContext { - workspace: WorkspaceIds - - lastTx?: string - - lastHash?: string - - hierarchy: Hierarchy - modelDb: ModelDb - branding: Branding | null - - adapterManager?: DBAdapterManager - storageAdapter?: StorageAdapter - serviceAdapterManager?: ServiceAdaptersManager - lowLevelStorage?: LowLevelStorage - liveQuery?: LiveQuery - queue?: PlatformQueue - queueProducers?: Map> - - // Entry point for derived data procvessing - derived?: Middleware - head?: Middleware - - contextVars: Record - - broadcastEvent?: (ctx: MeasureContext, tx: Tx[]) => Promise - userStatusMap?: Map, { online: boolean, user: AccountUuid }> -} -/** - * @public - */ -export interface Pipeline { - context: PipelineContext - findAll: ( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ) => Promise> - searchFulltext: ( - ctx: MeasureContext, - query: SearchQuery, - options: SearchOptions - ) => Promise - tx: (ctx: MeasureContext, tx: Tx[]) => Promise - close: () => Promise - - loadModel: ( - ctx: MeasureContext, - lastModelTx: Timestamp, - hash?: string - ) => Promise - - handleBroadcast: (ctx: MeasureContext) => Promise - - domainRequest: ( - ctx: MeasureContext, - domain: OperationDomain, - params: DomainParams - ) => Promise> - - closeSession: (ctx: MeasureContext, sessionId: string) => Promise -} - -/** - * @public - */ -export type PipelineFactory = ( - ctx: MeasureContext, - ws: WorkspaceIds, - broadcast: BroadcastOps, - branding: Branding | null -) => Promise - -export type CommunicationApiFactory = ( - ctx: MeasureContext, - ws: WorkspaceIds, - callbacks: CommunicationCallbacks -) => Promise - -/** - * @public - */ -export interface TriggerControl { - ctx: MeasureContext - workspace: WorkspaceIds - branding: Branding | null - txFactory: TxFactory - findAll: ( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ) => Promise> - hierarchy: Hierarchy - lowLevel: LowLevelStorage - modelDb: ModelDb - removedMap: Map, Doc> - userStatusMap: Map, { online: boolean, user: AccountUuid }> - domainRequest: (ctx: MeasureContext, domain: OperationDomain, params: DomainParams) => Promise - - queue?: PlatformQueue - - // Cache per workspace - cache: Map - // Cache per root tx - contextCache: Map - - // Since we don't have other storages let's consider adapter is MinioClient - // Later can be replaced with generic one with bucket encapsulated inside. - storageAdapter: StorageAdapter - serviceAdaptersManager: ServiceAdaptersManager - // Bulk operations in case trigger require some - apply: (ctx: MeasureContext, tx: Tx[], needResult?: boolean) => Promise - - // Will create a live query if missing and return values immediately if already asked. - queryFind: ( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ) => Promise> - - // Current set of transactions to being processed for apply/bulks - txes: Tx[] -} - -/** - * @public - */ -export type TriggerFunc = (tx: Tx[], ctrl: TriggerControl) => Promise - -/** - * @public - */ -export interface Trigger extends Doc { - trigger: Resource - - // In case defiled, trigger will be executed asyncronously after transaction will be done, trigger shouod use - isAsync?: boolean - - // We should match transaction - txMatch?: DocumentQuery -} - -/** - * @public - */ -export interface EmbeddingSearchOption { - field: string - field_enable: string - size?: number - from?: number - embeddingBoost?: number // default 100 - fulltextBoost?: number // default 10 - minScore?: number // 75 for example. -} - -/** - * @public - */ -export interface IndexedDoc { - id: Ref - _class: Ref>[] - space: Ref - modifiedOn: Timestamp - modifiedBy: PersonId - attachedTo?: Ref - attachedToClass?: Ref> - searchTitle?: string - searchTitle_fields?: any[] - searchShortTitle?: string - searchShortTitle_fields?: any[] - searchIcon_fields?: any[] - fulltextSummary?: string - [key: string]: any -} - -/** - * @public - */ -export interface SearchStringResult { - docs: IndexedDoc[] - total?: number -} - -export interface FulltextListener { - onIndexing?: (doc: IndexedDoc) => Promise - onClean?: (doc: Ref[]) => Promise -} - -/** - * @public - */ -export interface FullTextAdapter { - index: (ctx: MeasureContext, workspace: WorkspaceUuid, doc: IndexedDoc) => Promise - update: ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - id: Ref, - update: Record - ) => Promise - remove: (ctx: MeasureContext, workspace: WorkspaceUuid, id: Ref[]) => Promise - removeByQuery: (ctx: MeasureContext, workspace: WorkspaceUuid, query: DocumentQuery) => Promise - - clean: (ctx: MeasureContext, workspace: WorkspaceUuid) => Promise - updateMany: (ctx: MeasureContext, workspace: WorkspaceUuid, docs: IndexedDoc[]) => Promise - updateByQuery: ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - query: DocumentQuery, - update: Record - ) => Promise - load: (ctx: MeasureContext, workspace: WorkspaceUuid, docs: Ref[]) => Promise - searchString: ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - query: SearchQuery, - options: SearchOptions & { scoring?: SearchScoring[] } - ) => Promise - - search: ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - _classes: Ref>[], - search: DocumentQuery, - size: number | undefined, - from?: number - ) => Promise - - close: () => Promise - - // If no field is provided, will return existing mapping of all dimms. - initMapping: (ctx: MeasureContext, field?: { key: string, dims: number }) => Promise -} - -/** - * @public - */ -export type FullTextAdapterFactory = (url: string) => Promise - -/** - * @public - */ -export interface ContentTextAdapter { - content: (ctx: MeasureContext, workspace: WorkspaceUuid, name: string, type: string, doc: Buffer) => Promise -} - -/** - * @public - */ -export type ContentTextAdapterFactory = (url: string) => Promise - -/** - * @public - */ -export interface WithFind { - findAll: ( - ctx: MeasureContext, - clazz: Ref>, - query: DocumentQuery, - options?: FindOptions - ) => Promise> -} - -/** - * Allow to contribute and find all derived objects for document. - * @public - */ -export interface ObjectDDParticipant extends Class { - // Collect more items to be deleted if parent document is deleted. - collectDocs: Resource -} - -export type ObjectDDParticipantFunc = ( - doc: Doc, - hiearachy: Hierarchy, - findAll: ( - clazz: Ref>, - query: DocumentQuery, - options?: FindOptions - ) => Promise> -) => Promise - -/** - * @public - */ -export type SearchPresenterProvider = ( - doc: Doc, - parent: Doc | undefined, - space: Space | undefined, - hierarchy: Hierarchy, - mode: string -) => string - -export type FieldParamKind = 'space' | 'parent' - -export type FieldTemplateParam = - | /* field */ [string] - | [/* kind */ 'func', /* Presenter function */ Resource, /* mode */ string] - | [/* kind */ FieldParamKind, /* field */ string] -/** - * A concationation template, if string just put string, if obj, use source and field name. - * @public - */ -export type FieldTemplate = /* text */ (string | /* field inject */ FieldTemplateParam)[] -export interface FieldTemplateComponent { - component?: Resource - fields?: FieldTemplateParam[] // Extra fields - - template?: FieldTemplate // Primary value in index - extraFields?: FieldTemplate[] // Additional values in index. -} -/** - * @public - */ -export interface SearchScoring { - attr: string - value: string - boost: number -} - -/** - * @public - */ -export interface SearchPresenter extends Class { - searchIcon?: Asset - iconConfig?: FieldTemplateComponent - title: FieldTemplateComponent | FieldTemplate - shortTitle?: FieldTemplateComponent | FieldTemplate - scoring?: SearchScoring[] -} - -export interface ServiceAdapter { - close: () => Promise - metrics: () => MeasureContext -} - -export type ServiceAdapterFactory = (url: string, db: string, context: MeasureContext) => Promise - -export interface ServiceAdapterConfig { - factory: ServiceAdapterFactory - db: string - url: string -} - -export interface StorageConfig { - name: string - kind: string - endpoint: string - port?: number - readonly?: string -} - -export class NoSuchKeyError extends Error { - code: string - constructor ( - msg: string, - readonly cause?: any - ) { - super(msg) - this.code = 'NoSuchKey' - } -} - -export interface StorageConfiguration { - default: string - storages: StorageConfig[] -} - -/** - * @public - */ -export interface SessionRequest { - id: string - params: any - start: number -} - -export interface ClientSessionCtx { - ctx: MeasureContext - - pipeline: Pipeline - socialStringsToUsers: Map< - PersonId, - { - accontUuid: AccountUuid - role: AccountRole - } - > - requestId: ReqId | undefined - sendResponse: (id: ReqId | undefined, msg: any) => Promise - sendPong: () => void - sendError: (id: ReqId | undefined, msg: any, error: any) => Promise -} - -/** - * @public - */ -export interface Session { - workspace: WorkspaceIds - - token: Token - createTime: number - - // Session restore information - sessionId: string - sessionInstanceId?: string - workspaceClosed?: boolean - - requests: Map - - binaryMode: boolean - useCompression: boolean - total: StatisticsElement - current: StatisticsElement - mins5: StatisticsElement - - lastRequest: number - lastPing: number - - isUpgradeClient: () => boolean - - getMode: () => string - - broadcast: (ctx: MeasureContext, socket: ConnectionSocket, tx: Tx[]) => void - - // Client methods - ping: (ctx: ClientSessionCtx) => Promise - getUser: () => AccountUuid - - getUserSocialIds: () => PersonId[] - - getSocialIds: () => SocialId[] - - loadModel: (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string) => Promise - loadModelRaw: (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string) => Promise - getRawAccount: () => Account - findAll: ( - ctx: ClientSessionCtx, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ) => Promise - findAllRaw: ( - ctx: ClientSessionCtx, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ) => Promise> - searchFulltext: (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions) => Promise - searchFulltextRaw: (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions) => Promise - tx: (ctx: ClientSessionCtx, tx: Tx) => Promise - - txRaw: ( - ctx: ClientSessionCtx, - tx: Tx - ) => Promise<{ - result: TxResult - broadcastPromise: Promise - asyncsPromise: Promise | undefined - }> - - loadChunk: (ctx: ClientSessionCtx, domain: Domain, idx?: number) => Promise - - getDomainHash: (ctx: ClientSessionCtx, domain: Domain) => Promise - closeChunk: (ctx: ClientSessionCtx, idx: number) => Promise - loadDocs: (ctx: ClientSessionCtx, domain: Domain, docs: Ref[]) => Promise - upload: (ctx: ClientSessionCtx, domain: Domain, docs: Doc[]) => Promise - clean: (ctx: ClientSessionCtx, domain: Domain, docs: Ref[]) => Promise - - includeSessionContext: (ctx: ClientSessionCtx) => void - - updateLast: () => void - - domainRequestRaw: ( - ctx: ClientSessionCtx, - domain: OperationDomain, - params: DomainParams - ) => Promise<{ - result: DomainResult - broadcastPromise: Promise - asyncsPromise: Promise | undefined - }> -} - -/** - * @public - */ -export interface ConnectionSocket { - id: string - isClosed: boolean - close: () => void - send: (ctx: MeasureContext, msg: Response, binary: boolean, compression: boolean) => Promise - - sendPong: () => void - data: () => Record - - readRequest: (buffer: Buffer, binary: boolean) => Request - - isBackpressure: () => boolean // In bytes - backpressure: (ctx: MeasureContext) => Promise - checkState: () => boolean -} - -/** - * @public - */ -export let LOGGING_ENABLED = true - -/** - * @public - */ -export function disableLogging (): void { - LOGGING_ENABLED = false -} - -export interface AddSessionActive { - session: Session - context: MeasureContext - workspaceId: WorkspaceUuid -} - -export type GetWorkspaceResponse = - | { upgrade: true, progress?: number } - | { error: any, terminate?: boolean, specialError?: 'archived' | 'migration' } - -export type AddSessionResponse = AddSessionActive | GetWorkspaceResponse - -export type SessionHealth = 'healthy' | 'degraded' | 'unhealthy' - -/** - * @public - */ -export interface SessionManager { - // workspaces: Map - sessions: Map - - addSession: ( - ctx: MeasureContext, - ws: ConnectionSocket, - token: Token, - rawToken: string, - sessionId: string | undefined - ) => Promise - - broadcastAll: ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - tx: Tx[], - targets?: AccountUuid | AccountUuid[], - exclude?: AccountUuid[] - ) => void - - close: (ctx: MeasureContext, ws: ConnectionSocket, workspaceId: WorkspaceUuid) => Promise - - forceClose: (wsId: WorkspaceUuid, ignoreSocket?: ConnectionSocket) => Promise - - forceMaintenance: (ctx: MeasureContext, workspaceId: WorkspaceUuid) => Promise - - closeWorkspaces: (ctx: MeasureContext) => Promise - - scheduleMaintenance: (timeMinutes: number, message?: string) => void - - profiling?: { - start: () => void - stop: () => Promise - } - - handleRequest: ( - requestCtx: MeasureContext, - service: S, - ws: ConnectionSocket, - request: Request, - workspace: WorkspaceUuid - ) => Promise - - handleRPC: ( - requestCtx: MeasureContext, - service: S, - method: string, - ws: ConnectionSocket, - operation: (ctx: ClientSessionCtx, rateLimit?: RateLimitInfo) => Promise - ) => Promise - - createOpContext: ( - ctx: MeasureContext, - sendCtx: MeasureContext, - pipeline: Pipeline, - requestId: Request['id'], - service: Session, - ws: ConnectionSocket, - rateLimit?: RateLimitInfo - ) => ClientSessionCtx - - getStatistics: () => WorkspaceStatistics[] - - checkHealth: () => SessionHealth -} - -export const pingConst = 'ping' -export const pongConst = 'pong!' diff --git a/server/core/src/utils.ts b/server/core/src/utils.ts deleted file mode 100644 index 017b8282409..00000000000 --- a/server/core/src/utils.ts +++ /dev/null @@ -1,412 +0,0 @@ -import core, { - type AccountRole, - ClientConnectEvent, - WorkspaceEvent, - generateId, - getTypeOf, - systemAccount, - type Account, - type AccountUuid, - type BackupClient, - type BrandingMap, - type BulkUpdateEvent, - type Class, - type Client, - type ClientConnection, - type Doc, - type DocChunk, - type DocumentQuery, - type Domain, - type DomainParams, - type DomainResult, - type FindOptions, - type FindResult, - type MeasureContext, - type ModelDb, - type OperationDomain, - type PersonId, - type Ref, - type SearchResult, - type SessionData, - type Tx, - type TxResult, - type TxWorkspaceEvent, - type WorkspaceIds, - type PermissionsGrant, - toFindResult -} from '@hcengineering/core' -import { PlatformError, unknownError } from '@hcengineering/platform' -import { createHash, type Hash } from 'crypto' -import fs from 'fs' -import type { DbAdapter } from './adapter' -import { BackupClientOps } from './storage' -import type { Pipeline } from './types' - -/** - * Return some estimation for object size - */ -export function estimateDocSize (_obj: any): number { - let result = 0 - const toProcess = [_obj] - while (toProcess.length > 0) { - const obj = toProcess.shift() - if (typeof obj === 'undefined') { - continue - } - if (typeof obj === 'function') { - continue - } - for (const key in obj) { - // include prototype properties - const value = obj[key] - const type = getTypeOf(value) - result += key.length - - switch (type) { - case 'Array': - result += 4 - toProcess.push(value) - break - case 'Object': - toProcess.push(value) - break - case 'Date': - result += 24 // Some value - break - case 'string': - result += (value as string).length - break - case 'number': - result += 8 - break - case 'boolean': - result += 1 - break - case 'symbol': - result += (value as symbol).toString().length - break - case 'bigint': - result += (value as bigint).toString().length - break - case 'undefined': - result += 1 - break - case 'null': - result += 1 - break - default: - result += value.toString().length - } - } - } - return result -} -/** - * Calculate hash for object - */ -export function updateHashForDoc (hash: Hash, _obj: any): void { - const toProcess = [_obj] - while (toProcess.length > 0) { - const obj = toProcess.shift() - if (typeof obj === 'undefined') { - continue - } - if (typeof obj === 'function') { - continue - } - const keys = Object.keys(obj).sort() - // We need sorted list of keys to make it consistent - for (const key of keys) { - // include prototype properties - const value = obj[key] - const type = getTypeOf(value) - hash.update(key) - - switch (type) { - case 'Array': - toProcess.push(value) - break - case 'Object': - toProcess.push(value) - break - case 'Date': - hash.update(value.toString()) - break - case 'string': - hash.update(value) - break - case 'number': - hash.update((value as number).toString(16)) - break - case 'boolean': - hash.update((value as boolean) ? '1' : '0') - break - case 'symbol': - hash.update((value as symbol).toString()) - break - case 'bigint': - hash.update((value as bigint).toString()) - break - case 'undefined': - hash.update('und') - break - case 'null': - hash.update('null') - break - default: - hash.update(value.toString()) - } - } - } -} - -export class SessionDataImpl implements SessionData { - _removedMap: Map, Doc> | undefined - _contextCache: Map | undefined - _broadcast: SessionData['broadcast'] | undefined - - constructor ( - readonly account: Account, - readonly sessionId: string, - readonly admin: boolean | undefined, - _broadcast: SessionData['broadcast'] | undefined, - readonly workspace: WorkspaceIds, - readonly isAsyncContext: boolean, - _removedMap: Map, Doc> | undefined, - _contextCache: Map | undefined, - readonly modelDb: ModelDb, - readonly socialStringsToUsers: Map< - PersonId, - { - accontUuid: AccountUuid - role: AccountRole - } - >, - readonly service: string, - readonly grant?: PermissionsGrant - ) { - this._removedMap = _removedMap - this._contextCache = _contextCache - this._broadcast = _broadcast - } - - get broadcast (): SessionData['broadcast'] { - if (this._broadcast === undefined) { - this._broadcast = { - targets: {}, - txes: [], - queue: [], - sessions: {} - } - } - return this._broadcast - } - - get removedMap (): Map, Doc> { - if (this._removedMap === undefined) { - this._removedMap = new Map() - } - return this._removedMap - } - - get contextCache (): Map { - if (this._contextCache === undefined) { - this._contextCache = new Map() - } - return this._contextCache - } -} - -export function createBroadcastEvent (classes: Ref>[]): TxWorkspaceEvent { - return { - _class: core.class.TxWorkspaceEvent, - _id: generateId(), - event: WorkspaceEvent.BulkUpdate, - params: { - _class: classes - }, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - objectSpace: core.space.DerivedTx, - space: core.space.DerivedTx - } -} - -export function loadBrandingMap (brandingPath?: string): BrandingMap { - let brandings: BrandingMap = {} - if (brandingPath !== undefined && brandingPath !== '') { - brandings = JSON.parse(fs.readFileSync(brandingPath, 'utf8')) - - for (const [host, value] of Object.entries(brandings)) { - const protocol = value.protocol ?? 'https' - value.front = `${protocol}://${host}/` - } - } - - return brandings -} - -export function wrapPipeline ( - ctx: MeasureContext, - pipeline: Pipeline, - wsIds: WorkspaceIds, - doBroadcast: boolean = false -): Client & BackupClient { - const contextData = new SessionDataImpl( - systemAccount, - 'pipeline', - true, - { targets: {}, txes: [], queue: [], sessions: {} }, - wsIds, - true, - undefined, - undefined, - pipeline.context.modelDb, - new Map(), - 'transactor' - ) - ctx.contextData = contextData - if (pipeline.context.lowLevelStorage === undefined) { - throw new PlatformError(unknownError('Low level storage is not available')) - } - const backupOps = new BackupClientOps(pipeline.context.lowLevelStorage) - - return { - findAll: async (_class, query, options) => { - const result = await pipeline.findAll(ctx, _class, query, options) - return toFindResult( - result.map((v) => { - return pipeline.context.hierarchy.updateLookupMixin(_class, v, options) - }), - result.total - ) - }, - findOne: async (_class, query, options) => { - const result = await pipeline.findAll(ctx, _class, query, { ...options, limit: 1 }) - return toFindResult( - result.map((v) => { - return pipeline.context.hierarchy.updateLookupMixin(_class, v, options) - }), - result.total - )[0] - }, - domainRequest: async (domain, params) => { - return await pipeline.domainRequest(ctx, domain, params) - }, - clean: (domain, docs) => backupOps.clean(ctx, domain, docs), - close: () => pipeline.close(), - closeChunk: (idx) => backupOps.closeChunk(ctx, idx), - getHierarchy: () => pipeline.context.hierarchy, - getModel: () => pipeline.context.modelDb, - loadChunk: (domain, idx) => backupOps.loadChunk(ctx, domain, idx), - getDomainHash: (domain) => backupOps.getDomainHash(ctx, domain), - loadDocs: (domain, docs) => backupOps.loadDocs(ctx, domain, docs), - upload: (domain, docs) => backupOps.upload(ctx, domain, docs), - searchFulltext: async (query, options) => ({ docs: [], total: 0 }), - sendForceClose: async () => {}, - tx: async (tx) => { - const result = await pipeline.tx(ctx, [tx]) - if (doBroadcast) { - await pipeline.handleBroadcast(ctx) - } - return result - }, - notify: (...tx) => {} - } -} - -export function wrapAdapterToClient (ctx: MeasureContext, storageAdapter: DbAdapter, txes: Tx[]): ClientConnection { - class TestClientConnection implements ClientConnection { - isConnected = (): boolean => true - - handler?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise - - set onConnect ( - handler: ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined - ) { - this.handler = handler - void this.handler?.(ClientConnectEvent.Connected, '', {}) - } - - get onConnect (): ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined { - return this.handler - } - - pushHandler (): void {} - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - return (await storageAdapter.findAll(ctx, _class, query, options)) as any - } - - async domainRequest(domain: OperationDomain, params: DomainParams): Promise> { - return { domain, value: null as any } - } - - async tx (tx: Tx): Promise { - return await storageAdapter.tx(ctx, tx) - } - - async searchFulltext (): Promise { - return { docs: [] } - } - - async close (): Promise {} - - async loadChunk (domain: Domain): Promise { - throw new Error('unsupported') - } - - async getDomainHash (domain: Domain): Promise { - return await storageAdapter.getDomainHash(ctx, domain) - } - - async closeChunk (idx: number): Promise {} - - async loadDocs (domain: Domain, docs: Ref[]): Promise { - return [] - } - - async upload (domain: Domain, docs: Doc[]): Promise {} - - async clean (domain: Domain, docs: Ref[]): Promise {} - - async loadModel (): Promise { - return txes - } - - async sendForceClose (): Promise {} - } - return new TestClientConnection() -} - -export async function calcHashHash (ctx: MeasureContext, domain: Domain, adapter: DbAdapter): Promise { - const hash = createHash('sha256') - - const it = adapter.find(ctx, domain) - - try { - let count = 0 - while (true) { - const part = await it.next(ctx) - if (part.length === 0) { - break - } - count += part.length - for (const doc of part) { - hash.update(doc.id) - hash.update(doc.hash) - } - } - if (count === 0) { - // Use empty hash for empty documents. - return '' - } - return hash.digest('hex') - } finally { - await it.close(ctx) - } -} diff --git a/server/core/tsconfig.json b/server/core/tsconfig.json deleted file mode 100644 index c6a877cf6c3..00000000000 --- a/server/core/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/datalake/.eslintrc.js b/server/datalake/.eslintrc.js deleted file mode 100644 index ce90fb9646f..00000000000 --- a/server/datalake/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/datalake/.npmignore b/server/datalake/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/datalake/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/datalake/config/rig.json b/server/datalake/config/rig.json deleted file mode 100644 index 78cc5a17334..00000000000 --- a/server/datalake/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/server/datalake/jest.config.js b/server/datalake/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/datalake/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/datalake/package.json b/server/datalake/package.json deleted file mode 100644 index 77e2340ec46..00000000000 --- a/server/datalake/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@hcengineering/datalake", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "@types/node": "^22.15.29", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "ts-node": "^10.8.0" - }, - "dependencies": { - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "@hcengineering/server-token": "^0.7.0" - } -} diff --git a/server/datalake/src/__tests__/utils.test.ts b/server/datalake/src/__tests__/utils.test.ts deleted file mode 100644 index ab989104073..00000000000 --- a/server/datalake/src/__tests__/utils.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { wrapETag, unwrapETag } from '../utils' - -describe('unwrapETag', () => { - it('should unwrap weak validator prefix', () => { - expect(unwrapETag('W/"abc"')).toBe('abc') - }) - - it('should unwrap strong validator prefix', () => { - expect(unwrapETag('"abc"')).toBe('abc') - }) - - it('should unwrap no validator prefix', () => { - expect(unwrapETag('abc')).toBe('abc') - }) -}) - -describe('wrapETag', () => { - it('should wrap strong validator prefix', () => { - expect(wrapETag('abc')).toBe('"abc"') - }) - - it('should wrap weak validator prefix', () => { - expect(wrapETag('abc', true)).toBe('W/"abc"') - }) -}) diff --git a/server/datalake/src/client.ts b/server/datalake/src/client.ts deleted file mode 100644 index 6c734b6b874..00000000000 --- a/server/datalake/src/client.ts +++ /dev/null @@ -1,545 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type MeasureContext, type WorkspaceUuid, concatLink } from '@hcengineering/core' -import { Readable } from 'stream' - -import { DatalakeError, NetworkError, NotFoundError } from './error' -import { unwrapETag } from './utils' - -/** @public */ -export interface ObjectMetadata { - lastModified: number - name: string - contentType: string - etag: string - size?: number -} - -/** @public */ -export interface ListObjectOutput { - cursor: string | undefined - blobs: Omit[] -} - -/** @public */ -export interface StatObjectOutput { - lastModified: number - type: string - etag?: string - size?: number -} - -/** @public */ -export interface UploadObjectParams { - lastModified: number - type: string - size?: number -} - -interface BlobUploadError { - key: string - error: string -} - -interface BlobUploadSuccess { - key: string - id: string - metadata: ObjectMetadata -} - -type BlobUploadResult = BlobUploadSuccess | BlobUploadError - -interface MultipartUpload { - uploadId: string -} - -interface MultipartUploadPart { - partNumber: number - etag: string -} - -export interface R2UploadParams { - location: string - bucket: string -} - -/** @public */ -export interface WorkspaceStats { - count: number - size: number -} - -/** @public */ -export class DatalakeClient { - private readonly headers: Record - - constructor ( - private readonly endpoint: string, - private readonly token: string - ) { - this.headers = { Authorization: 'Bearer ' + token } - } - - getObjectUrl (ctx: MeasureContext, workspace: WorkspaceUuid, objectName: string): string { - const path = `/blob/${workspace}/${encodeURIComponent(objectName)}` - return concatLink(this.endpoint, path) - } - - async getWorkspaceStats (ctx: MeasureContext, workspace: WorkspaceUuid): Promise { - const path = `/stats/${workspace}` - const url = new URL(concatLink(this.endpoint, path)) - const response = await fetchSafe(ctx, url, { headers: { ...this.headers } }) - return (await response.json()) as WorkspaceStats - } - - async listObjects ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - cursor: string | undefined, - limit: number = 100 - ): Promise { - const path = `/blob/${workspace}` - const url = new URL(concatLink(this.endpoint, path)) - url.searchParams.append('limit', String(limit)) - if (cursor !== undefined) { - url.searchParams.append('cursor', cursor) - } - - const response = await fetchSafe(ctx, url, { headers: { ...this.headers } }) - return (await response.json()) as ListObjectOutput - } - - async getObject (ctx: MeasureContext, workspace: WorkspaceUuid, objectName: string): Promise { - const url = this.getObjectUrl(ctx, workspace, objectName) - - let response - try { - response = await fetchSafe(ctx, url, { headers: { ...this.headers } }) - } catch (err: any) { - if (err.name === 'NotFoundError') { - return undefined - } - throw err - } - - if (response.body == null) { - ctx.error('bad datalake response', { objectName }) - throw new DatalakeError('Missing response body') - } - - return Readable.fromWeb(response.body as any) - } - - async getPartialObject ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - objectName: string, - offset: number, - length?: number - ): Promise { - const url = this.getObjectUrl(ctx, workspace, objectName) - const headers = { - ...this.headers, - Range: length !== undefined ? `bytes=${offset}-${offset + length - 1}` : `bytes=${offset}` - } - - let response - try { - response = await fetchSafe(ctx, url, { headers }) - } catch (err: any) { - if (err.name === 'NotFoundError') { - return undefined - } - throw err - } - - if (response.body == null) { - ctx.error('bad datalake response', { objectName }) - throw new DatalakeError('Missing response body') - } - - return Readable.fromWeb(response.body as any) - } - - async statObject ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - objectName: string - ): Promise { - const url = this.getObjectUrl(ctx, workspace, objectName) - - let response: Response - try { - response = await fetchSafe(ctx, url, { - method: 'HEAD', - headers: { ...this.headers } - }) - } catch (err: any) { - if (err.name === 'NotFoundError') { - return - } - throw err - } - - const headers = response.headers - const lastModified = Date.parse(headers.get('Last-Modified') ?? '') - const size = parseInt(headers.get('Content-Length') ?? '0', 10) - - return { - lastModified: isNaN(lastModified) ? 0 : lastModified, - size: isNaN(size) ? 0 : size, - type: headers.get('Content-Type') ?? '', - etag: unwrapETag(headers.get('ETag') ?? '') - } - } - - async deleteObject (ctx: MeasureContext, workspace: WorkspaceUuid, objectName: string): Promise { - const url = this.getObjectUrl(ctx, workspace, objectName) - try { - await fetchSafe(ctx, url, { - method: 'DELETE', - headers: { ...this.headers } - }) - } catch (err: any) { - if (err.name !== 'NotFoundError') { - throw err - } - } - } - - async putObject ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - objectName: string, - stream: Readable | Buffer | string, - params: UploadObjectParams - ): Promise { - let size = params.size - if (size === undefined) { - if (Buffer.isBuffer(stream)) { - size = stream.length - } else if (typeof stream === 'string') { - size = Buffer.byteLength(stream) - } else { - // TODO: Implement size calculation for Readable streams - ctx.warn('unknown object size', { workspace, objectName }) - } - } - - if (size === undefined || size < 64 * 1024 * 1024) { - return await ctx.with( - 'direct-upload', - {}, - (ctx) => this.uploadWithFormData(ctx, workspace, objectName, stream, { ...params, size }), - { workspace, objectName } - ) - } else { - return await ctx.with( - 'multipart-upload', - {}, - (ctx) => this.uploadWithMultipart(ctx, workspace, objectName, stream, { ...params, size }), - { workspace, objectName } - ) - } - } - - async uploadWithFormData ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - objectName: string, - stream: Readable | Buffer | string, - params: UploadObjectParams - ): Promise { - const path = `/upload/form-data/${workspace}` - const url = concatLink(this.endpoint, path) - - const buffer = await toBuffer(stream) - const file = new File([new Uint8Array(buffer)], objectName, { - type: params.type, - lastModified: params.lastModified - }) - - const form = new FormData() - form.append('file', file) - - const response = await fetchSafe(ctx, url, { - method: 'POST', - body: form as unknown as BodyInit, - headers: { - ...this.headers - } - }) - - const result = (await response.json()) as BlobUploadResult[] - if (result.length !== 1) { - throw new DatalakeError('Bad datalake response: ' + result.toString()) - } - - const uploadResult = result[0] - - if ('error' in uploadResult) { - throw new DatalakeError('Upload failed: ' + uploadResult.error) - } - - return uploadResult.metadata - } - - async uploadWithMultipart ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - objectName: string, - stream: Readable | Buffer | string, - params: UploadObjectParams - ): Promise { - const chunkSize = 10 * 1024 * 1024 - - const multipart = await this.multipartUploadStart(ctx, workspace, objectName, params) - - try { - const parts: MultipartUploadPart[] = [] - - let partNumber = 1 - for await (const chunk of getChunks(stream, chunkSize)) { - const part = await this.multipartUploadPart(ctx, workspace, objectName, multipart, partNumber, chunk) - parts.push(part) - partNumber++ - } - - return await this.multipartUploadComplete(ctx, workspace, objectName, multipart, parts) - } catch (err: any) { - await this.multipartUploadAbort(ctx, workspace, objectName, multipart) - throw err - } - } - - // S3 - - async uploadFromS3 ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - objectName: string, - params: { - url: string - accessKeyId: string - secretAccessKey: string - } - ): Promise { - const path = `/upload/s3/${workspace}/${encodeURIComponent(objectName)}` - const url = concatLink(this.endpoint, path) - - await fetchSafe(ctx, url, { - method: 'POST', - headers: { - ...this.headers, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(params) - }) - } - - async getS3UploadParams (ctx: MeasureContext, workspace: WorkspaceUuid): Promise { - const path = `/upload/s3/${workspace}` - const url = concatLink(this.endpoint, path) - - const response = await fetchSafe(ctx, url, { headers: { ...this.headers } }) - const json = (await response.json()) as R2UploadParams - return json - } - - async createFromS3 ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - objectName: string, - params: { - filename: string - } - ): Promise { - const path = `/upload/s3/${workspace}/${encodeURIComponent(objectName)}` - const url = concatLink(this.endpoint, path) - - await fetchSafe(ctx, url, { - method: 'POST', - headers: { - ...this.headers, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(params) - }) - } - - // Multipart - - private async multipartUploadStart ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - objectName: string, - params: UploadObjectParams - ): Promise { - const path = `/upload/multipart/${workspace}/${encodeURIComponent(objectName)}` - const url = concatLink(this.endpoint, path) - - try { - const headers = { - ...this.headers, - 'Content-Type': params.type, - 'Content-Length': params.size?.toString() ?? '0', - 'Last-Modified': new Date(params.lastModified).toUTCString() - } - const response = await fetchSafe(ctx, url, { method: 'POST', headers }) - return (await response.json()) as MultipartUpload - } catch (err: any) { - ctx.error('failed to start multipart upload', { workspace, objectName, err }) - throw new DatalakeError('Failed to start multipart upload') - } - } - - private async multipartUploadPart ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - objectName: string, - multipart: MultipartUpload, - partNumber: number, - data: Readable | Buffer | string - ): Promise { - const path = `/upload/multipart/${workspace}/${encodeURIComponent(objectName)}/part` - const url = new URL(concatLink(this.endpoint, path)) - url.searchParams.set('uploadId', multipart.uploadId) - url.searchParams.set('partNumber', partNumber.toString()) - - const body = data instanceof Readable ? (Readable.toWeb(data) as ReadableStream) : data - - try { - const response = await fetchSafe(ctx, url, { - method: 'PUT', - body: body as BodyInit, - headers: { ...this.headers } - }) - return (await response.json()) as MultipartUploadPart - } catch (err: any) { - ctx.error('failed to upload multipart part', { workspace, objectName, err }) - throw new DatalakeError('Failed to upload multipart part') - } - } - - private async multipartUploadComplete ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - objectName: string, - multipart: MultipartUpload, - parts: MultipartUploadPart[] - ): Promise { - const path = `/upload/multipart/${workspace}/${encodeURIComponent(objectName)}/complete` - const url = new URL(concatLink(this.endpoint, path)) - url.searchParams.set('uploadId', multipart.uploadId) - - try { - const res = await fetchSafe(ctx, url, { - method: 'POST', - body: JSON.stringify({ parts }), - headers: { - 'Content-Type': 'application/json', - ...this.headers - } - }) - return (await res.json()) as ObjectMetadata - } catch (err: any) { - ctx.error('failed to complete multipart upload', { workspace, objectName, err }) - throw new DatalakeError('Failed to complete multipart upload') - } - } - - private async multipartUploadAbort ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - objectName: string, - multipart: MultipartUpload - ): Promise { - const path = `/upload/multipart/${workspace}/${encodeURIComponent(objectName)}/abort` - const url = new URL(concatLink(this.endpoint, path)) - url.searchParams.set('uploadId', multipart.uploadId) - - try { - await fetchSafe(ctx, url, { method: 'POST', headers: { ...this.headers } }) - } catch (err: any) { - ctx.error('failed to abort multipart upload', { workspace, objectName, err }) - throw new DatalakeError('Failed to abort multipart upload') - } - } -} - -async function toBuffer (data: Buffer | string | Readable): Promise { - if (Buffer.isBuffer(data)) { - return data - } else if (typeof data === 'string') { - return Buffer.from(data) - } else if (data instanceof Readable) { - const chunks: Buffer[] = [] - for await (const chunk of data) { - chunks.push(chunk) - } - return Buffer.concat(chunks as any) - } else { - throw new TypeError('Unsupported data type') - } -} - -async function * getChunks (data: Buffer | string | Readable, chunkSize: number): AsyncGenerator { - if (Buffer.isBuffer(data)) { - let offset = 0 - while (offset < data.length) { - yield data.subarray(offset, offset + chunkSize) - offset += chunkSize - } - } else if (typeof data === 'string') { - const buffer = Buffer.from(data) - yield * getChunks(buffer, chunkSize) - } else if (data instanceof Readable) { - let buffer = Buffer.alloc(0) - - for await (const chunk of data) { - buffer = Buffer.concat([buffer, chunk]) - - while (buffer.length >= chunkSize) { - yield buffer.subarray(0, chunkSize) - buffer = buffer.subarray(chunkSize) - } - } - if (buffer.length > 0) { - yield buffer - } - } -} - -async function fetchSafe (ctx: MeasureContext, url: string | URL, init?: RequestInit): Promise { - let response - try { - response = await ctx.with('fetch', {}, () => fetch(url, init), { url: url.toString() }, { span: 'disable' }) - } catch (err: any) { - ctx.error('network error', { err }) - throw new NetworkError(`Network error ${err}`) - } - - if (!response.ok) { - const text = await response.text() - if (response.status === 404) { - throw new NotFoundError(text) - } else { - throw new DatalakeError(text) - } - } - - return response -} diff --git a/server/datalake/src/error.ts b/server/datalake/src/error.ts deleted file mode 100644 index b5b60262b14..00000000000 --- a/server/datalake/src/error.ts +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export class NetworkError extends Error { - constructor (message: string) { - super(message) - this.name = 'NetworkError' - } -} - -export class DatalakeError extends Error { - constructor (message: string) { - super(message) - this.name = 'DatalakeError' - } -} - -export class NotFoundError extends DatalakeError { - constructor (message = 'Not Found') { - super(message) - this.name = 'NotFoundError' - } -} diff --git a/server/datalake/src/index.ts b/server/datalake/src/index.ts deleted file mode 100644 index 1884e6b09a8..00000000000 --- a/server/datalake/src/index.ts +++ /dev/null @@ -1,289 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type Blob, - type MeasureContext, - type Ref, - type WorkspaceIds, - systemAccountUuid, - withContext -} from '@hcengineering/core' -import { getMetadata } from '@hcengineering/platform' -import { - type BlobStorageIterator, - type BucketInfo, - type ListBlobResult, - type StorageAdapter, - type StorageConfig, - type StorageConfiguration, - type UploadedObjectInfo -} from '@hcengineering/server-core' -import serverToken, { generateToken } from '@hcengineering/server-token' -import { type Readable } from 'stream' -import { type UploadObjectParams, DatalakeClient, type WorkspaceStats } from './client' -import { NotFoundError } from './error' - -export { DatalakeClient, type WorkspaceStats } - -/** - * @public - */ -export interface DatalakeConfig extends StorageConfig { - kind: 'datalake' -} - -/** - * @public - */ -export function createDatalakeClient (cfg: DatalakeConfig, token: string): DatalakeClient { - const endpoint = Number.isInteger(cfg.port) ? `${cfg.endpoint}:${cfg.port}` : cfg.endpoint - return new DatalakeClient(endpoint, token) -} - -export const CONFIG_KIND = 'datalake' - -/** - * @public - */ -export interface DatalakeClientOptions { - retryCount?: number - retryInterval?: number -} - -/** - * @public - */ -export class DatalakeService implements StorageAdapter { - private readonly client: DatalakeClient - private readonly retryCount: number - private readonly retryInterval: number - - constructor ( - readonly cfg: DatalakeConfig, - readonly options: DatalakeClientOptions = {} - ) { - const secret = getMetadata(serverToken.metadata.Secret) - if (secret === undefined) { - console.warn('Server secret not set, datalake storage adapter initialized with default secret') - } - const token = generateToken(systemAccountUuid, undefined) - this.client = createDatalakeClient(cfg, token) - this.retryCount = options.retryCount ?? 5 - this.retryInterval = options.retryInterval ?? 50 - } - - async initialize (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} - - async close (): Promise {} - - async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - // workspace/buckets not supported, assume that always exist - return true - } - - @withContext('make') - async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - // workspace/buckets not supported, assume that always exist - } - - async listBuckets (ctx: MeasureContext): Promise { - return [] - } - - @withContext('remove') - async remove (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]): Promise { - await Promise.all( - objectNames.map(async (objectName) => { - await this.retry(ctx, () => this.client.deleteObject(ctx, wsIds.uuid, objectName)) - }) - ) - } - - @withContext('delete') - async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - // not supported, just do nothing and pretend we deleted the workspace - } - - @withContext('listStream') - async listStream (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - let hasMore = true - const buffer: ListBlobResult[] = [] - let cursor: string | undefined - - return { - next: async () => { - try { - while (hasMore && buffer.length < 50) { - const res = await this.retry(ctx, () => this.client.listObjects(ctx, wsIds.uuid, cursor, 10000)) - hasMore = res.cursor !== undefined - cursor = res.cursor - - for (const blob of res.blobs) { - buffer.push({ - _id: blob.name as Ref, - _class: core.class.Blob, - etag: blob.etag, - size: (typeof blob.size === 'string' ? parseInt(blob.size) : blob.size) ?? 0, - provider: this.cfg.name, - space: core.space.Configuration, - modifiedBy: core.account.System, - modifiedOn: 0, - contentType: blob.contentType - }) - } - } - } catch (err: any) { - ctx.error('Failed to get list', { error: err, workspace: wsIds.uuid }) - } - return buffer.splice(0, 50) - }, - close: async () => {} - } - } - - @withContext('stat') - async stat (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - const result = await this.retry(ctx, () => this.client.statObject(ctx, wsIds.uuid, objectName)) - if (result !== undefined) { - return { - provider: '', - _class: core.class.Blob, - _id: objectName as Ref, - contentType: result.type, - size: result.size ?? 0, - etag: result.etag ?? '', - space: core.space.Configuration, - modifiedBy: core.account.System, - modifiedOn: result.lastModified, - version: null - } - } - } - - @withContext('get') - async get (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - const object = await this.retry(ctx, () => this.client.getObject(ctx, wsIds.uuid, objectName)) - if (object === undefined) { - throw new NotFoundError() - } - return object - } - - @withContext('put') - async put ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - stream: Readable | Buffer | string, - contentType: string, - size?: number - ): Promise { - const params: UploadObjectParams = { - lastModified: Date.now(), - type: contentType, - size - } - - const { etag } = await ctx.with( - 'put', - {}, - (ctx) => this.retry(ctx, () => this.client.putObject(ctx, wsIds.uuid, objectName, stream, params)), - { workspace: wsIds.uuid, objectName } - ) - - return { - etag, - versionId: '' - } - } - - @withContext('read') - async read (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - const data = await this.retry(ctx, () => this.client.getObject(ctx, wsIds.uuid, objectName)) - if (data === undefined) { - throw new NotFoundError() - } - - const chunks: Buffer[] = [] - for await (const chunk of data) { - chunks.push(chunk) - } - - return chunks - } - - @withContext('partial') - async partial ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - offset: number, - length?: number - ): Promise { - const object = await this.retry(ctx, () => - this.client.getPartialObject(ctx, wsIds.uuid, objectName, offset, length) - ) - if (object === undefined) { - throw new NotFoundError() - } - return object - } - - async getUrl (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - return this.client.getObjectUrl(ctx, wsIds.uuid, objectName) - } - - async retry(ctx: MeasureContext, op: () => Promise): Promise { - return await withRetry(ctx, this.retryCount, op, this.retryInterval) - } -} - -export function processConfigFromEnv (storageConfig: StorageConfiguration): string | undefined { - const endpoint = process.env.DATALAKE_ENDPOINT - if (endpoint === undefined) { - return 'DATALAKE_ENDPOINT' - } - - const config: DatalakeConfig = { - kind: 'datalake', - name: 'datalake', - endpoint - } - storageConfig.storages.push(config) - storageConfig.default = 'datalake' -} - -async function withRetry ( - ctx: MeasureContext, - retries: number, - op: () => Promise, - delay: number = 100 -): Promise { - let error: any - while (retries > 0) { - retries-- - try { - return await op() - } catch (err: any) { - error = err - ctx.error('error', { err }) - if (retries !== 0 && delay > 0) { - await new Promise((resolve) => setTimeout(resolve, delay)) - } - } - } - throw error -} diff --git a/server/datalake/src/perfTest.ts b/server/datalake/src/perfTest.ts deleted file mode 100644 index 2932c09f8a0..00000000000 --- a/server/datalake/src/perfTest.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { MeasureMetricsContext, type WorkspaceDataId, type WorkspaceUuid, generateId } from '@hcengineering/core' -import type { StorageConfiguration } from '@hcengineering/server-core' -import { DatalakeService, processConfigFromEnv, type DatalakeConfig } from '.' - -const MB = 1024 * 1024 - -const config: StorageConfiguration = { default: 'minio', storages: [] } -const minioConfigVar = processConfigFromEnv(config) -if (minioConfigVar !== undefined || config.storages[0] === undefined) { - console.error('No Datalake config env is configured:' + minioConfigVar) - it.skip('No Datalake config env is configured', async () => {}) - process.exit(1) -} -const toolCtx = new MeasureMetricsContext('test', {}) -const storageService = new DatalakeService({ ...(config.storages[0] as DatalakeConfig) }) - -async function doTest (): Promise { - const genWorkspaceId1 = generateId() as unknown as WorkspaceDataId - - const wsIds1 = { - uuid: genWorkspaceId1 as unknown as WorkspaceUuid, - dataId: genWorkspaceId1, - url: '' - } - await storageService.make(toolCtx, wsIds1) - - /// /////// Uploads - console.log('upload 1mb test') - let st1 = Date.now() - const sz = 10 - const stream = Buffer.alloc(sz * 1024 * 1024) - for (let i = 0; i < 10; i++) { - // We need 1Mb random file to check upload speed. - const st = Date.now() - await storageService.put(toolCtx, wsIds1, `testObject.${i}`, stream, 'application/octet-stream', stream.length) - console.log('upload time', Date.now() - st) - } - let now = Date.now() - console.log(`upload performance: ${Math.round((sz * 10 * 1000 * 100) / (now - st1)) / 100} mb per second`) - - /// // Downloads 1 - console.log('download 1mb test') - st1 = Date.now() - for (let i = 0; i < 10; i++) { - // We need 1Mb random file to check upload speed. - const st = Date.now() - await storageService.read(toolCtx, wsIds1, `testObject.${i}`) - console.log('download time', Date.now() - st) - } - - now = Date.now() - console.log(`download performance: ${Math.round((sz * 10 * 1000 * 100) / (now - st1)) / 100} mb per second`) - - /// Downloads 2 - st1 = Date.now() - for (let i = 0; i < 10; i++) { - // We need 1Mb random file to check upload speed. - const st = Date.now() - const readable = await storageService.get(toolCtx, wsIds1, `testObject.${i}`) - const chunks: Buffer[] = [] - readable.on('data', (chunk) => { - chunks.push(chunk) - }) - await new Promise((resolve) => { - readable.on('end', () => { - resolve() - readable.destroy() - }) - }) - console.log('download time 2', Date.now() - st) - } - - now = Date.now() - console.log(`download performance: ${Math.round((sz * 10 * 1000 * 100) / (now - st1)) / 100} mb per second`) - - /// Downloads 3 - console.log('download partial test') - st1 = Date.now() - for (let i = 0; i < 10; i++) { - // We need 1Mb random file to check upload speed. - const st = Date.now() - for (let i = 0; i < sz; i++) { - const readable = await storageService.partial(toolCtx, wsIds1, `testObject.${i}`, i * MB, MB) - const chunks: Buffer[] = [] - readable.on('data', (chunk) => { - chunks.push(chunk) - }) - await new Promise((resolve) => { - readable.on('end', () => { - resolve() - readable.destroy() - }) - }) - } - console.log('download time 2', Date.now() - st) - } - - now = Date.now() - console.log(`download performance: ${Math.round((sz * 10 * 1000 * 100) / (now - st1)) / 100} mb per second`) -} - -void doTest().catch((err) => { - console.error(err) -}) - -console.log('done') diff --git a/server/datalake/src/utils.ts b/server/datalake/src/utils.ts deleted file mode 100644 index 42d60ff1283..00000000000 --- a/server/datalake/src/utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export function unwrapETag (etag: string): string { - if (etag.startsWith('W/')) { - etag = etag.substring(2) - } - - if (etag.startsWith('"') && etag.endsWith('"')) { - etag = etag.slice(1, -1) - } - - return etag -} - -export function wrapETag (etag: string, weak: boolean = false): string { - etag = unwrapETag(etag) - const quoted = etag.startsWith('"') ? etag : `"${etag}"` - return weak ? `W/${quoted}` : quoted -} diff --git a/server/datalake/tsconfig.json b/server/datalake/tsconfig.json deleted file mode 100644 index c6a877cf6c3..00000000000 --- a/server/datalake/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/elastic/.eslintrc.js b/server/elastic/.eslintrc.js deleted file mode 100644 index 72235dc2833..00000000000 --- a/server/elastic/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/elastic/.npmignore b/server/elastic/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/elastic/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/elastic/config/rig.json b/server/elastic/config/rig.json deleted file mode 100644 index 0110930f55e..00000000000 --- a/server/elastic/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/server/elastic/jest.config.js b/server/elastic/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/elastic/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/elastic/package.json b/server/elastic/package.json deleted file mode 100644 index 5f2c71d93bb..00000000000 --- a/server/elastic/package.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "name": "@hcengineering/elastic", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "init": "ts-node src/__init.ts", - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "ts-node": "^10.8.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" - }, - "dependencies": { - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "@elastic/elasticsearch": "^7.17.14", - "@hcengineering/analytics": "^0.7.3" - }, - "repository": "https://github.com/hcengineering/platform", - "publishConfig": { - "access": "public" - } -} diff --git a/server/elastic/src/__tests__/adapter.test.ts b/server/elastic/src/__tests__/adapter.test.ts deleted file mode 100644 index c08d5c9641d..00000000000 --- a/server/elastic/src/__tests__/adapter.test.ts +++ /dev/null @@ -1,63 +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 { Class, Doc, MeasureMetricsContext, PersonId, Ref, Space, WorkspaceUuid } from '@hcengineering/core' -import type { FullTextAdapter, IndexedDoc } from '@hcengineering/server-core' - -import { createElasticAdapter } from '../adapter' - -describe('Elastic Adapter', () => { - let adapter: FullTextAdapter - const ctx = new MeasureMetricsContext('-', {}) - const ws1 = 'ws1' as WorkspaceUuid - beforeEach(async () => { - adapter = await createElasticAdapter(process.env.ELASTIC_URL ?? 'http://localhost:9200/') - }) - - afterEach(async () => { - await adapter.close() - }) - - it('should init', () => { - expect(adapter).toBeTruthy() - }) - - it('should create document', async () => { - const doc: IndexedDoc = { - id: 'doc1' as Ref, - _class: ['class1' as Ref>], - modifiedBy: 'andrey' as PersonId, - modifiedOn: 0, - space: 'space1' as Ref, - content0: 'hey there!' - } - await adapter.index(ctx, ws1, doc) - const hits = await adapter.search(ctx, ws1, ['class1' as Ref>], {}, 1) - console.log(hits) - }) - - it('should find document with raw search', async () => { - const result = await adapter.searchString( - ctx, - ws1, - { - query: 'hey' - }, - {} - ) - console.log(result) - }) -}) diff --git a/server/elastic/src/adapter.ts b/server/elastic/src/adapter.ts deleted file mode 100644 index 1da9e3711e6..00000000000 --- a/server/elastic/src/adapter.ts +++ /dev/null @@ -1,707 +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 { Analytics } from '@hcengineering/analytics' -import { - Class, - Doc, - DocumentQuery, - MeasureContext, - Ref, - SearchOptions, - SearchQuery, - TxResult, - WorkspaceUuid -} from '@hcengineering/core' -import type { FullTextAdapter, IndexedDoc, SearchScoring, SearchStringResult } from '@hcengineering/server-core' -import serverCore from '@hcengineering/server-core' - -import { Client, errors as esErr } from '@elastic/elasticsearch' -import { getMetadata } from '@hcengineering/platform' - -const DEFAULT_LIMIT = 200 - -function getIndexName (): string { - return getMetadata(serverCore.metadata.ElasticIndexName) ?? 'storage_index' -} - -function getIndexVersion (): string { - return getMetadata(serverCore.metadata.ElasticIndexVersion) ?? 'v2' -} - -const mappings = { - properties: { - fulltextSummary: { - type: 'text', - analyzer: 'rebuilt_english' - }, - workspaceId: { - type: 'keyword', - index: true - }, - id: { - type: 'keyword', - index: true - }, - _class: { - type: 'keyword', - index: true - }, - attachedTo: { - type: 'keyword', - index: true - }, - attachedToClass: { - type: 'keyword', - index: true - }, - space: { - type: 'keyword', - index: true - }, - 'core:class:Doc%createdBy': { - type: 'keyword', - index: true - }, - 'core:class:Doc%createdOn': { - type: 'date', - format: 'epoch_millis', - index: true - }, - modifiedBy: { - type: 'keyword', - index: true - }, - modifiedOn: { - type: 'date', - format: 'epoch_millis', - index: true - }, - 'core:class:Doc%modifiedBy': { - type: 'keyword', - index: true - }, - 'core:class:Doc%modifiedOn': { - type: 'date', - format: 'epoch_millis', - index: true - } - } -} - -class ElasticAdapter implements FullTextAdapter { - private readonly getFulltextDocId: (workspaceId: WorkspaceUuid, doc: Ref) => Ref - private readonly getDocId: (workspaceId: WorkspaceUuid, fulltext: Ref) => Ref - private readonly indexName: string - - constructor ( - private readonly client: Client, - private readonly indexBaseName: string, - readonly indexVersion: string - ) { - this.indexName = `${indexBaseName}_${indexVersion}` - this.getFulltextDocId = (workspaceId, doc) => `${doc}@${workspaceId}` as Ref - this.getDocId = (workspaceId, fulltext) => fulltext.slice(0, -1 * (workspaceId.length + 1)) as Ref - } - - async initMapping (ctx: MeasureContext): Promise { - const indexName = this.indexName - try { - const existingVersions = await ctx.withSync('get-indexes', {}, () => - this.client.indices.get({ - index: [`${this.indexBaseName}_*`] - }) - ) - const allIndexes = Object.keys(existingVersions.body) - const existingOldVersionIndices = allIndexes.filter((name) => name !== indexName) - const existsIndex = allIndexes.find((it) => it === indexName) !== undefined - let shouldDropExistingIndex = false - if (existsIndex) { - const mapping = await ctx.with('get-mapping', { indexName }, () => - this.client.indices.getMapping({ - index: indexName - }) - ) - for (const [propName, propType] of Object.entries(mappings.properties)) { - if (mapping.body[indexName]?.mappings.properties?.[propName]?.type !== propType.type) { - shouldDropExistingIndex = true - break - } - } - } - if (existingOldVersionIndices.length > 0 || shouldDropExistingIndex) { - await ctx.with('delete-old-index', {}, () => - this.client.indices.delete({ - index: shouldDropExistingIndex ? allIndexes : existingOldVersionIndices - }) - ) - } - if (!existsIndex || shouldDropExistingIndex) { - await ctx.with('create-index', { indexName }, () => - this.client.indices.create({ - index: indexName, - body: { - settings: { - analysis: { - filter: { - english_stemmer: { - type: 'stemmer', - language: 'english' - }, - english_possessive_stemmer: { - type: 'stemmer', - language: 'possessive_english' - } - }, - analyzer: { - rebuilt_english: { - type: 'custom', - tokenizer: 'standard', - filter: ['english_possessive_stemmer', 'lowercase', 'english_stemmer'] - } - } - } - }, - mappings - } - }) - ) - } else { - await ctx.with('put-mapping', {}, () => - this.client.indices.putMapping({ - index: indexName, - body: mappings - }) - ) - } - } catch (err: any) { - if (err.name === 'ConnectionError') { - ctx.warn('Elastic DB is not available') - } - Analytics.handleError(err) - ctx.error(err) - return false - } - return true - } - - async close (): Promise { - await this.client.close() - } - - async searchString ( - ctx: MeasureContext, - workspaceId: WorkspaceUuid, - query: SearchQuery, - options: SearchOptions & { scoring?: SearchScoring[] } - ): Promise { - try { - const elasticQuery: any = { - query: { - function_score: { - query: { - bool: { - must: [ - { - simple_query_string: { - query: query.query, - analyze_wildcard: true, - flags: 'OR|PREFIX|PHRASE|FUZZY|NOT|ESCAPE', - default_operator: 'and', - fields: [ - 'searchTitle^50', // boost - 'searchShortTitle^50', - '*' // Search in all other fields without a boost - ] - } - }, - { - term: { - workspaceId - } - } - ] - } - }, - boost_mode: 'sum' - } - }, - size: options.limit ?? DEFAULT_LIMIT - } - - const filter: any = [ - { - exists: { field: 'searchTitle' } - } - ] - - if (query.spaces !== undefined) { - filter.push({ - terms: this.getTerms(query.spaces, 'space') - }) - } - if (query.classes !== undefined) { - filter.push({ - terms: this.getTerms(query.classes, '_class') - }) - } - - if (filter.length > 0) { - elasticQuery.query.function_score.query.bool.filter = filter - } - - if (options.scoring !== undefined) { - const scoringTerms: any[] = options.scoring.map((scoringOption): any => { - const field = Object.hasOwn(mappings.properties, scoringOption.attr) - ? scoringOption.attr - : `${scoringOption.attr}.keyword` - return { - term: { - [field]: { - value: scoringOption.value, - boost: scoringOption.boost - } - } - } - }) - elasticQuery.query.function_score.query.bool.should = scoringTerms - } - - const result = await this.client.search({ - index: this.indexName, - body: elasticQuery - }) - - const resp: SearchStringResult = { docs: [] } - if (result.body.hits !== undefined) { - if (result.body.hits.total?.value !== undefined) { - resp.total = result.body.hits.total?.value - } - resp.docs = result.body.hits.hits.map((hit: any) => ({ ...hit._source, _score: hit._score })) - } - - return resp - } catch (err: any) { - if (err.name === 'ConnectionError') { - ctx.warn('Elastic DB is not available') - return { docs: [] } - } - Analytics.handleError(err) - ctx.error('Elastic error', { error: err }) - return { docs: [] } - } - } - - async search ( - ctx: MeasureContext, - workspaceId: WorkspaceUuid, - _classes: Ref>[], - query: DocumentQuery, - size: number | undefined, - from: number | undefined - ): Promise { - if (query.$search === undefined) return [] - const request: any = { - bool: { - must: [ - { - simple_query_string: { - query: query.$search, - analyze_wildcard: true, - flags: 'OR|PREFIX|PHRASE|FUZZY|NOT|ESCAPE', - default_operator: 'and' - } - }, - { - term: { - workspaceId - } - } - ], - should: [{ terms: this.getTerms(_classes, '_class', { boost: 10.0 }) }], - filter: [ - { - bool: { - should: [ - { terms: this.getTerms(_classes, '_class') } - // { terms: this.getTerms(_classes, 'attachedToClass') } - ] - } - } - ] - } - } - - for (const [q, v] of Object.entries(query)) { - if (!q.startsWith('$')) { - const field = Object.hasOwn(mappings.properties, q) ? q : `${q}.keyword` - if (typeof v === 'object') { - if (v.$in !== undefined) { - request.bool.should.push({ - terms: { - [field]: v.$in, - boost: 100.0 - } - }) - } - } else { - request.bool.should.push({ - term: { - [field]: { - value: v, - boost: 100.0, - case_insensitive: true - } - } - }) - } - } - } - - try { - const result = await ctx.with( - 'search', - {}, - () => - this.client.search({ - index: this.indexName, - body: { - query: request, - size: size ?? 200, - from: from ?? 0 - } - }), - { - _classes, - size, - from, - query: request - } - ) - const hits = result.body.hits.hits as any[] - return hits.map((hit) => ({ ...hit._source, _score: hit._score })) - } catch (err: any) { - if (err.name === 'ConnectionError') { - ctx.warn('Elastic DB is not available') - return [] - } - ctx.error('Elastic error', { error: err }) - Analytics.handleError(err) - return [] - } - } - - private getTerms (values: string[], field: string, extra: any = {}): any { - return { - [Object.hasOwn(mappings.properties, field) ? field : `${field}.keyword`]: values, - ...extra - } - } - - async index (ctx: MeasureContext, workspaceId: WorkspaceUuid, doc: IndexedDoc): Promise { - const wsDoc = { - workspaceId, - ...doc - } - const fulltextId = this.getFulltextDocId(workspaceId, doc.id) - if (doc.data === undefined) { - await this.client.index({ - index: this.indexName, - id: fulltextId, - type: '_doc', - body: wsDoc - }) - } else { - await this.client.index({ - index: this.indexName, - id: fulltextId, - type: '_doc', - pipeline: 'attachment', - body: wsDoc - }) - } - return {} - } - - async update ( - ctx: MeasureContext, - workspaceId: WorkspaceUuid, - id: Ref, - update: Record - ): Promise { - await this.client.update({ - index: this.indexName, - id: this.getFulltextDocId(workspaceId, id), - body: { - doc: update - } - }) - - return {} - } - - async updateMany (ctx: MeasureContext, workspaceId: WorkspaceUuid, docs: IndexedDoc[]): Promise { - const parts = Array.from(docs) - while (parts.length > 0) { - const part = parts.splice(0, 500) - - const operations = part.flatMap((doc) => { - const wsDoc = { workspaceId, ...doc } - return [ - { index: { _index: this.indexName, _id: this.getFulltextDocId(workspaceId, doc.id) } }, - { ...wsDoc, type: '_doc' } - ] - }) - - const response = await this.client.bulk({ refresh: true, body: operations }) - if ((response as any).body.errors === true) { - const errors = response.body.items.filter((it: any) => it.index.error !== undefined) - const errorIds = new Set(errors.map((it: any) => it.index._id)) - const erroDocs = docs.filter((it) => errorIds.has(it.id)) - // Collect only errors - const errs = Array.from( - errors.map((it: any) => { - return `${it.index.error.reason}: ${it.index.error.caused_by?.reason}` - }) - ).join('\n') - - console.error(`Failed to process bulk request: ${errs} ${JSON.stringify(erroDocs)}`) - } - } - return [] - } - - async updateByQuery ( - ctx: MeasureContext, - workspaceId: WorkspaceUuid, - query: DocumentQuery, - update: Record - ): Promise { - const elasticQuery: any = { - bool: { - must: [ - { - term: { - workspaceId - } - } - ] - } - } - - for (const [q, v] of Object.entries(query)) { - if (!q.startsWith('$')) { - if (typeof v === 'object') { - if (v.$in !== undefined) { - elasticQuery.bool.must.push({ - terms: { - [Object.hasOwn(mappings.properties, q) ? q : `${q}.keyword`]: v.$in - } - }) - } - } else { - elasticQuery.bool.must.push({ - term: { - [Object.hasOwn(mappings.properties, q) ? q : `${q}.keyword`]: { - value: v - } - } - }) - } - } - } - - await this.client.updateByQuery({ - type: '_doc', - index: this.indexName, - body: { - query: elasticQuery, - script: { - source: - 'for(int i = 0; i < params.updateFields.size(); i++) { ctx._source[params.updateFields[i].key] = params.updateFields[i].value }', - params: { - updateFields: Object.entries(update).map(([key, value]) => ({ key, value })) - }, - lang: 'painless' - } - } - }) - return [] - } - - async remove (ctx: MeasureContext, workspaceId: WorkspaceUuid, docs: Ref[]): Promise { - try { - while (docs.length > 0) { - const part = docs.splice(0, 5000) - await this.client.deleteByQuery( - { - type: '_doc', - index: this.indexName, - body: { - query: { - bool: { - must: [ - { - terms: { - _id: part.map((it) => this.getFulltextDocId(workspaceId, it)), - boost: 1.0 - } - }, - { - term: { - workspaceId - } - } - ] - } - }, - size: part.length - } - }, - undefined - ) - } - } catch (e: any) { - if (e instanceof esErr.ResponseError && e.meta.statusCode === 404) { - return - } - throw e - } - } - - async removeByQuery (ctx: MeasureContext, workspaceId: WorkspaceUuid, query: DocumentQuery): Promise { - const elasticQuery: any = { - bool: { - must: [ - { - term: { - workspaceId - } - } - ] - } - } - - for (const [q, v] of Object.entries(query)) { - if (!q.startsWith('$')) { - if (typeof v === 'object') { - if (v.$in !== undefined) { - elasticQuery.bool.must.push({ - terms: { - [Object.hasOwn(mappings.properties, q) ? q : `${q}.keyword`]: v.$in - } - }) - } - } else { - elasticQuery.bool.must.push({ - term: { - [Object.hasOwn(mappings.properties, q) ? q : `${q}.keyword`]: { - value: v - } - } - }) - } - } - } - try { - await this.client.deleteByQuery({ - type: '_doc', - index: this.indexName, - body: { - query: elasticQuery - } - }) - } catch (e: any) { - if (e instanceof esErr.ResponseError && e.meta.statusCode === 404) { - return - } - throw e - } - } - - async clean (ctx: MeasureContext, workspaceId: WorkspaceUuid): Promise { - try { - await this.client.deleteByQuery( - { - type: '_doc', - index: this.indexName, - body: { - query: { - bool: { - must: [ - { - term: { - workspaceId - } - } - ] - } - } - } - }, - undefined - ) - } catch (e: any) { - if (e instanceof esErr.ResponseError && e.meta.statusCode === 404) { - return - } - throw e - } - } - - async load (ctx: MeasureContext, workspaceId: WorkspaceUuid, docs: Ref[]): Promise { - const resp = await this.client.search({ - index: this.indexName, - type: '_doc', - body: { - query: { - bool: { - must: [ - { - terms: { - _id: docs.map((it) => this.getFulltextDocId(workspaceId, it)), - boost: 1.0 - } - }, - { - term: { - workspaceId - } - } - ] - } - }, - size: docs.length - } - }) - return Array.from( - resp.body.hits.hits.map((hit: any) => ({ ...hit._source, id: this.getDocId(workspaceId, hit._id) })) - ) - } -} - -/** - * @public - */ -export async function createElasticAdapter (url: string): Promise { - const client = new Client({ - node: url - }) - const indexBaseName = getIndexName() - const indexVersion = getIndexVersion() - - return new ElasticAdapter(client, indexBaseName, indexVersion) -} diff --git a/server/elastic/src/index.ts b/server/elastic/src/index.ts deleted file mode 100644 index 7da598e6324..00000000000 --- a/server/elastic/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021, 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export { createElasticAdapter } from './adapter' diff --git a/server/elastic/tsconfig.json b/server/elastic/tsconfig.json deleted file mode 100644 index b5ae22f6e46..00000000000 --- a/server/elastic/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/kafka/.eslintrc.js b/server/kafka/.eslintrc.js deleted file mode 100644 index ce90fb9646f..00000000000 --- a/server/kafka/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/kafka/.npmignore b/server/kafka/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/kafka/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/kafka/config/rig.json b/server/kafka/config/rig.json deleted file mode 100644 index 78cc5a17334..00000000000 --- a/server/kafka/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/server/kafka/jest.config.js b/server/kafka/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/kafka/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/kafka/package.json b/server/kafka/package.json deleted file mode 100644 index 8785beae729..00000000000 --- a/server/kafka/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@hcengineering/kafka", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "@types/node": "^22.15.29", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" - }, - "dependencies": { - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "@hcengineering/storage": "^0.7.3", - "kafkajs": "^2.2.4" - } -} diff --git a/server/kafka/src/__test__/queue.spec.ts b/server/kafka/src/__test__/queue.spec.ts deleted file mode 100644 index b9223ac7675..00000000000 --- a/server/kafka/src/__test__/queue.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { generateId, MeasureMetricsContext, type WorkspaceUuid } from '@hcengineering/core' -import { createPlatformQueue, parseQueueConfig } from '..' - -jest.setTimeout(120000) -const testCtx = new MeasureMetricsContext('test', {}) -describe('queue', () => { - it('check-queue', async () => { - const genId = generateId() - const queue = createPlatformQueue(parseQueueConfig('localhost:19093;-queue_testing-' + genId, 'test-' + genId, '')) - const docsCount = 100 - try { - let msgCount = 0 - const p1 = new Promise((resolve, reject) => { - const to = setTimeout(() => { - reject(new Error(`Timeout waiting for messages:${msgCount}`)) - }, 100000) - queue.createConsumer(testCtx, 'qtest', genId, async (ctx, msg) => { - msgCount += 1 - console.log('msgCount', msgCount) - if (msgCount === docsCount) { - clearTimeout(to) - resolve() - } - }) - }) - - const producer = queue.getProducer(testCtx, 'qtest') - for (let i = 0; i < docsCount; i++) { - await producer.send(testCtx, genId as any as WorkspaceUuid, ['msg' + i]) - } - - await p1 - } catch (err: any) { - console.log(err) - } finally { - await queue.shutdown() - await queue.deleteTopics(['test']) - } - }) - - it('check-processing-errors', async () => { - const genId = generateId() - const queue = createPlatformQueue(parseQueueConfig('localhost:19093;-queue_testing-' + genId, 'test-' + genId, '')) - - try { - let counter = 2 - const p = new Promise((resolve, reject) => { - queue.createConsumer(testCtx, 'test', genId, async (ctx, msg) => { - counter-- - if (counter > 0) { - throw new Error('Processing Error') - } - resolve() - }) - }) - - const producer = queue.getProducer(testCtx, 'test') - await producer.send(testCtx, genId as any as WorkspaceUuid, ['msg']) - - await p - } finally { - await queue.shutdown() - await queue.deleteTopics(['test']) - } - }) -}) diff --git a/server/kafka/src/index.ts b/server/kafka/src/index.ts deleted file mode 100644 index 302b23abff0..00000000000 --- a/server/kafka/src/index.ts +++ /dev/null @@ -1,333 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type MeasureContext, type WorkspaceUuid } from '@hcengineering/core' -import { - QueueTopic, - type ConsumerControl, - type ConsumerHandle, - type ConsumerMessage, - type PlatformQueue, - type PlatformQueueProducer -} from '@hcengineering/server-core' -import { Kafka, Partitioners, type Consumer, type Producer } from 'kafkajs' -import type * as tls from 'tls' - -export interface QueueConfig { - postfix: string // a topic prefix to be used do distinguish between staging and production topics in same broker - brokers: string[] - clientId: string - region: string - - ssl?: tls.ConnectionOptions | boolean -} - -/** - * Query config parser in the form of 'brokers;clientId' - * brokers are comma separated - */ -export function parseQueueConfig (config: string, serviceId: string, region: string): QueueConfig { - const [brokers, postfix] = config.split(';') - return { - postfix: postfix ?? '', // Empty by default - brokers: brokers.split(','), - clientId: serviceId, - region - } -} - -function getKafkaTopicId (topic: QueueTopic | string, config: QueueConfig): string { - if (config.region !== '') { - return `${config.region}.${topic}${config.postfix ?? ''}` - } - return `${topic}${config.postfix ?? ''}` -} - -class PlatformQueueImpl implements PlatformQueue { - consumers: ConsumerHandle[] = [] - producers = new Map() - constructor ( - private readonly kafka: Kafka, - readonly config: QueueConfig - ) {} - - getClientId (): string { - return this.config.clientId - } - - async shutdown (): Promise { - for (const [, p] of this.producers) { - try { - await p.close() - } catch (err: any) { - console.error('failed to close producer', err) - } - } - for (const c of this.consumers) { - try { - await c.close() - } catch (err: any) { - console.error('failed to close consumer', err) - } - } - } - - getProducer(ctx: MeasureContext, topic: QueueTopic | string): PlatformQueueProducer { - const producer = this.producers.get(topic) - if (producer !== undefined && !producer.isClosed()) return producer - - const created = new PlatformQueueProducerImpl(ctx, this.kafka, getKafkaTopicId(topic, this.config), this) - this.producers.set(topic, created) - - return created - } - - createConsumer( - ctx: MeasureContext, - topic: QueueTopic | string, - groupId: string, - onMessage: (ctx: MeasureContext, msg: ConsumerMessage, queue: ConsumerControl) => Promise, - options?: { - fromBegining?: boolean - } - ): ConsumerHandle { - const result = new PlatformQueueConsumerImpl(ctx, this.kafka, this.config, topic, groupId, onMessage, options) - this.consumers.push(result) - return result - } - - async checkCreateTopic (topic: QueueTopic | string, topics: Set, numPartitions?: number): Promise { - const kTopic = getKafkaTopicId(topic, this.config) - if (!topics.has(kTopic)) { - try { - await this.kafka.admin().createTopics({ topics: [{ topic: kTopic, numPartitions: numPartitions ?? 1 }] }) - } catch (err: any) { - console.error('Failed to create topic', kTopic, err) - } - } - } - - async createTopic (topics: string | string[], partitions: number): Promise { - const existing = new Set(await this.kafka.admin({}).listTopics()) - topics = Array.isArray(topics) ? topics : [topics] - for (const topic of topics) { - await this.checkCreateTopic(topic, existing, partitions) - } - } - - async createTopics (tx: number): Promise { - const topics = new Set(await this.kafka.admin({}).listTopics()) - await this.checkCreateTopic(QueueTopic.Tx, topics, tx) - await this.checkCreateTopic(QueueTopic.Fulltext, topics, 1) - await this.checkCreateTopic(QueueTopic.Workspace, topics, 1) - await this.checkCreateTopic(QueueTopic.Users, topics, 1) - await this.checkCreateTopic(QueueTopic.Process, topics, 1) - } - - async checkDeleteTopic (topic: QueueTopic | string, topics: Set): Promise { - const kTopic = getKafkaTopicId(topic, this.config) - if (topics.has(kTopic)) { - try { - await this.kafka.admin().deleteTopics({ topics: [kTopic] }) - } catch (err: any) { - console.error('Failed to delete topic', kTopic, err) - } - } - } - - async deleteTopics (topics?: (QueueTopic | string)[]): Promise { - const existing = new Set(await this.kafka.admin({}).listTopics()) - if (topics !== undefined) { - for (const t of topics) { - await this.checkDeleteTopic(t, existing) - } - } else { - await this.checkDeleteTopic(QueueTopic.Tx, existing) - await this.checkDeleteTopic(QueueTopic.Fulltext, existing) - await this.checkDeleteTopic(QueueTopic.Workspace, existing) - await this.checkDeleteTopic(QueueTopic.Users, existing) - } - } -} - -class PlatformQueueProducerImpl implements PlatformQueueProducer { - txProducer: Producer - connected: Promise | undefined - private closed = false - - constructor ( - readonly ctx: MeasureContext, - kafka: Kafka, - private readonly topic: string, - private readonly queue: PlatformQueue - ) { - this.txProducer = kafka.producer({ - allowAutoTopicCreation: true, - createPartitioner: Partitioners.DefaultPartitioner - }) - this.connected = this.ctx.with('connect-broker', {}, () => this.txProducer.connect()) - } - - getQueue (): PlatformQueue { - return this.queue - } - - async send (ctx: MeasureContext, workspace: WorkspaceUuid, msgs: any[], partitionKey?: string): Promise { - if (this.connected !== undefined) { - await this.connected - this.connected = undefined - } - await this.ctx.with('send', { topic: this.topic }, () => - this.txProducer.send({ - topic: this.topic, - messages: msgs.map((m) => ({ - key: Buffer.from(`${partitionKey ?? workspace}`), - value: Buffer.from(JSON.stringify(m)), - headers: { - workspace, - meta: JSON.stringify(ctx.extractMeta()) - } - })) - }) - ) - } - - isClosed (): boolean { - return this.closed - } - - async close (): Promise { - this.closed = true - await this.ctx.with('disconnect', {}, () => this.txProducer.disconnect()) - } -} - -class PlatformQueueConsumerImpl implements ConsumerHandle { - connected = false - cc: Consumer - constructor ( - readonly ctx: MeasureContext, - readonly kafka: Kafka, - readonly config: QueueConfig, - private readonly topic: QueueTopic | string, - groupId: string, - private readonly onMessage: ( - ctx: MeasureContext, - msg: ConsumerMessage, - queue: ConsumerControl - ) => Promise, - private readonly options?: { - fromBegining?: boolean - } - ) { - this.cc = this.kafka.consumer({ - groupId: `${getKafkaTopicId(this.topic, this.config)}-${groupId}`, - allowAutoTopicCreation: true - }) - - void this.start().catch((err) => { - ctx.error('failed to consume', { err }) - }) - } - - async start (): Promise { - await this.doConnect() - await this.doSubscribe() - - await this.cc.run({ - eachMessage: async ({ topic, message, pause, heartbeat }) => { - const msgKey = message.key?.toString() ?? '' - const msgData = JSON.parse(message.value?.toString() ?? '{}') - const meta = JSON.parse(message.headers?.meta?.toString() ?? '{}') - const workspace = (message.headers?.workspace?.toString() ?? msgKey) as WorkspaceUuid - - let to = 1 - while (true) { - try { - await this.ctx.with( - 'handle-msg', - {}, - (ctx) => this.onMessage(ctx, { workspace, value: msgData }, { heartbeat, pause }), - {}, - { - meta - } - ) - break - } catch (err: any) { - this.ctx.error('failed to process message', { err, msgKey, msgData, workspace }) - await heartbeat() - await new Promise((resolve) => setTimeout(resolve, to * 1000)) - if (to < 10) { - to++ - } - } - } - } - }) - } - - async doConnect (): Promise { - this.cc.on('consumer.connect', () => { - this.connected = true - this.ctx.info('consumer connected to queue') - }) - this.cc.on('consumer.disconnect', () => { - this.connected = false - this.ctx.warn('consumer disconnected from queue') - }) - await this.cc.connect() - } - - async doSubscribe (): Promise { - await this.cc.subscribe({ - topic: getKafkaTopicId(this.topic, this.config), - fromBeginning: this.options?.fromBegining - }) - } - - isConnected (): boolean { - return this.connected - } - - close (): Promise { - return this.cc.disconnect() - } -} - -/** - * Constructs a platform queue. - */ -export function getPlatformQueue (serviceId: string, region?: string): PlatformQueue { - const queueConfig = process.env.QUEUE_CONFIG ?? 'huly.local:9092' - if (queueConfig === undefined) { - throw new Error('Please provide queue config') - } - const config = parseQueueConfig(queueConfig, serviceId, region ?? process.env.REGION ?? '') - return createPlatformQueue(config) -} - -export function createPlatformQueue (config: QueueConfig): PlatformQueue { - console.info({ message: 'Using queue', config }) - - return new PlatformQueueImpl( - new Kafka({ - clientId: config.clientId, - brokers: config.brokers, - ssl: config.ssl - }), - config - ) -} diff --git a/server/kafka/tsconfig.json b/server/kafka/tsconfig.json deleted file mode 100644 index c6a877cf6c3..00000000000 --- a/server/kafka/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/middleware/.eslintrc.js b/server/middleware/.eslintrc.js deleted file mode 100644 index ce90fb9646f..00000000000 --- a/server/middleware/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/middleware/.npmignore b/server/middleware/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/middleware/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/middleware/config/rig.json b/server/middleware/config/rig.json deleted file mode 100644 index 78cc5a17334..00000000000 --- a/server/middleware/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/server/middleware/jest.config.js b/server/middleware/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/middleware/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/middleware/package.json b/server/middleware/package.json deleted file mode 100644 index 77b4ad72eb3..00000000000 --- a/server/middleware/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "@hcengineering/middleware", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --forceExit", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/node": "^22.15.29" - }, - "dependencies": { - "@hcengineering/ai-bot": "^0.7.0", - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "@hcengineering/server-preference": "^0.7.0", - "@hcengineering/query": "^0.7.3", - "@hcengineering/analytics": "^0.7.3", - "@hcengineering/card": "^0.7.0", - "fast-equals": "^5.2.2" - } -} diff --git a/server/middleware/src/applyTx.ts b/server/middleware/src/applyTx.ts deleted file mode 100644 index e1275ef2533..00000000000 --- a/server/middleware/src/applyTx.ts +++ /dev/null @@ -1,138 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type MeasureContext, - type Tx, - type TxApplyIf, - type TxApplyResult, - type TxResult -} from '@hcengineering/core' -import type { Middleware, PipelineContext, TxMiddlewareResult } from '@hcengineering/server-core' -import { BaseMiddleware } from '@hcengineering/server-core' - -/** - * Will support apply tx - * @public - */ -export class ApplyTxMiddleware extends BaseMiddleware implements Middleware { - scopes = new Map>() - - static async create (ctx: MeasureContext, context: PipelineContext, next?: Middleware): Promise { - return new ApplyTxMiddleware(context, next) - } - - async tx (ctx: MeasureContext, txes: Tx[]): Promise { - const result: TxResult[] = [] - - let part: Tx[] = [] - for (const tx of txes) { - if (this.context.hierarchy.isDerived(tx._class, core.class.TxApplyIf)) { - if (part.length > 0) { - part = [] - result.push(await this.provideTx(ctx, part)) - } - const applyIf = tx as TxApplyIf - // Wait for scope promise if found - const passed = - applyIf.scope != null ? await this.verifyApplyIf(ctx, applyIf) : { passed: true, onEnd: () => {} } - try { - if (passed.passed) { - const applyResult: TxApplyResult = { - success: true, - serverTime: 0 - } - result.push(applyResult) - - const st = Date.now() - const r = await this.provideTx(ctx, applyIf.txes) - if (Object.keys(r).length > 0) { - result.push(r) - } - applyResult.serverTime = Date.now() - st - } else { - result.push({ - success: false - }) - } - } finally { - passed.onEnd() - } - } else { - part.push(tx) - } - } - if (part.length > 0) { - result.push(await this.provideTx(ctx, part)) - } - if (Array.isArray(result) && result.length === 1) { - return result[0] - } - return result - } - - /** - * Verify if apply if is possible to apply. - */ - async verifyApplyIf ( - ctx: MeasureContext, - applyIf: TxApplyIf - ): Promise<{ - onEnd: () => void - passed: boolean - }> { - if (applyIf.scope == null) { - return { passed: true, onEnd: () => {} } - } - // Wait for synchronized. - const scopePromise = this.scopes.get(applyIf.scope) - - if (scopePromise != null) { - await scopePromise - } - - let onEnd = (): void => {} - // Put sync code - this.scopes.set( - applyIf.scope, - new Promise((resolve) => { - onEnd = () => { - this.scopes.delete(applyIf.scope as unknown as string) - resolve(null) - } - }) - ) - let passed = true - if (applyIf.match != null) { - for (const { _class, query } of applyIf.match) { - const res = await this.provideFindAll(ctx, _class, query, { limit: 1 }) - if (res.length === 0) { - passed = false - break - } - } - } - if (passed && applyIf.notMatch != null) { - for (const { _class, query } of applyIf.notMatch) { - const res = await this.provideFindAll(ctx, _class, query, { limit: 1 }) - if (res.length > 0) { - passed = false - break - } - } - } - return { passed, onEnd } - } -} diff --git a/server/middleware/src/broadcast.ts b/server/middleware/src/broadcast.ts deleted file mode 100644 index 030086dd5dc..00000000000 --- a/server/middleware/src/broadcast.ts +++ /dev/null @@ -1,176 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type BroadcastExcludeResult, - type BroadcastResult, - TxProcessor, - type AccountUuid, - type BroadcastTargets, - type Class, - type Doc, - type MeasureContext, - type Ref, - type SessionData, - type Tx, - type TxCUD -} from '@hcengineering/core' -import type { - BroadcastOps, - Middleware, - MiddlewareCreator, - PipelineContext, - TxMiddlewareResult -} from '@hcengineering/server-core' -import { BaseMiddleware, createBroadcastEvent } from '@hcengineering/server-core' - -/** - * @public - */ -export class BroadcastMiddleware extends BaseMiddleware implements Middleware { - constructor ( - context: PipelineContext, - protected readonly next: Middleware | undefined, - readonly broadcast: BroadcastOps - ) { - super(context, next) - context.broadcastEvent = (ctx, tx) => this.doBroadcast(ctx, tx) - } - - static create (broadcast: BroadcastOps): MiddlewareCreator { - return async (ctx, pipelineContext, next) => new BroadcastMiddleware(pipelineContext, next, broadcast) - } - - async handleBroadcast (ctx: MeasureContext): Promise { - await this.next?.handleBroadcast(ctx) - - await this.doBroadcast(ctx, ctx.contextData.broadcast.txes, ctx.contextData.broadcast.targets) - - if (Object.keys(ctx.contextData.broadcast.sessions).length > 0) { - this.broadcast.broadcastSessions(ctx, ctx.contextData.broadcast.sessions) - } - } - - tx (ctx: MeasureContext, tx: Tx[]): Promise { - // We collect all broadcast information here, so we could send it later - ctx.contextData.broadcast.txes.push(...tx) - - return this.provideTx(ctx, tx) - } - - async doBroadcast (ctx: MeasureContext, tx: Tx[], targets?: BroadcastTargets): Promise { - if (tx.length === 0) { - return - } - - // Combine targets by sender - - const toSendTarget = new Map() - const txesWithExcludedAccounts = new Map() - - const getTxes = (key: AccountUuid | ''): Tx[] => { - let txes = toSendTarget.get(key) - if (txes === undefined) { - txes = [...(toSendTarget.get('') ?? [])] // We also need to add all from to all - toSendTarget.set(key, txes) - } - return txes - } - - // Put current user as send target - for (const txd of tx) { - let target: BroadcastResult - for (const tt of Object.values(targets ?? {})) { - target = await tt(txd) - if (target !== undefined) { - break - } - } - if (target === undefined) { - getTxes('') // Be sure we have empty one - - // Also add to all other targeted sends - for (const v of toSendTarget.values()) { - v.push(txd) - } - } else { - if (isExlcude(target)) { - txesWithExcludedAccounts.set(txd, target.exclude) - } else { - for (const t of target.target) { - getTxes(t).push(txd) - } - } - } - } - - const handleSend = async ( - ctx: MeasureContext, - derived: Tx[], - target?: AccountUuid, - exclude?: AccountUuid[] - ): Promise => { - if (derived.length === 0) { - return - } - - if (derived.length > 10000) { - await this.sendWithPart(derived, ctx, target, exclude) - } else { - // Let's send after our response will go out - this.broadcast.broadcast(ctx, derived, target, exclude) - } - } - - const toSendAll = toSendTarget.get('') ?? [] - toSendTarget.delete('') - - // Then send targeted and all other - for (const [k, v] of toSendTarget.entries()) { - void handleSend(ctx, v, k as AccountUuid) - } - - for (const [tx, txExcludedAccounts] of txesWithExcludedAccounts.entries()) { - void handleSend(ctx, [tx], undefined, txExcludedAccounts) - } - - // Send all other except us. - await handleSend(ctx, toSendAll, undefined, Array.from(toSendTarget.keys()) as AccountUuid[]) - } - - private async sendWithPart ( - derived: Tx[], - ctx: MeasureContext, - target: AccountUuid | undefined, - exclude: AccountUuid[] | undefined - ): Promise { - const classes = new Set>>() - for (const dtx of derived) { - if (TxProcessor.isExtendsCUD(dtx._class)) { - classes.add((dtx as TxCUD).objectClass) - const attachedToClass = (dtx as TxCUD).attachedToClass - if (attachedToClass !== undefined) { - classes.add(attachedToClass) - } - } - } - const bevent = createBroadcastEvent(Array.from(classes)) - this.broadcast.broadcast(ctx, [bevent], target, exclude) - } -} - -function isExlcude (result: BroadcastResult): result is BroadcastExcludeResult { - return (result as BroadcastExcludeResult).exclude !== undefined -} diff --git a/server/middleware/src/configuration.ts b/server/middleware/src/configuration.ts deleted file mode 100644 index 1b57d497de7..00000000000 --- a/server/middleware/src/configuration.ts +++ /dev/null @@ -1,71 +0,0 @@ -// -// 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 { - AccountRole, - type Doc, - DOMAIN_CONFIGURATION, - type MeasureContext, - type Tx, - type TxCUD, - TxProcessor, - type SessionData -} from '@hcengineering/core' -import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import { - BaseMiddleware, - type Middleware, - type TxMiddlewareResult, - type PipelineContext -} from '@hcengineering/server-core' - -export const configurationAccountEmail = '#configurator@hc.engineering' -/** - * @public - */ -export class ConfigurationMiddleware extends BaseMiddleware implements Middleware { - private readonly targetDomains = [DOMAIN_CONFIGURATION] - - private constructor ( - readonly context: PipelineContext, - next?: Middleware - ) { - super(context, next) - } - - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new ConfigurationMiddleware(context, next) - } - - tx (ctx: MeasureContext, txes: Tx[]): Promise { - for (const tx of txes) { - if (TxProcessor.isExtendsCUD(tx._class)) { - const txCUD = tx as TxCUD - const domain = this.context.hierarchy.getDomain(txCUD.objectClass) - if (this.targetDomains.includes(domain)) { - const account = ctx.contextData.account - if (account.role !== AccountRole.Owner && ctx.contextData.admin !== true) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) - } - } - } - } - return this.provideTx(ctx, txes) - } -} diff --git a/server/middleware/src/contextName.ts b/server/middleware/src/contextName.ts deleted file mode 100644 index 1fb3f0eceff..00000000000 --- a/server/middleware/src/contextName.ts +++ /dev/null @@ -1,82 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - registerOperationLog, - updateOperationLog, - type DomainParams, - type MeasureContext, - type Metrics, - type OperationDomain, - type OperationLog, - type SessionData, - type Tx, - type TxApplyIf -} from '@hcengineering/core' -import type { DomainResult } from '@hcengineering/core/src' -import type { Middleware, PipelineContext, TxMiddlewareResult } from '@hcengineering/server-core' -import { BaseMiddleware } from '@hcengineering/server-core' - -/** - * Will support apply tx - * @public - */ -export class ContextNameMiddleware extends BaseMiddleware implements Middleware { - scopes = new Map>() - - static async create (ctx: MeasureContext, context: PipelineContext, next?: Middleware): Promise { - return new ContextNameMiddleware(context, next) - } - - domainRequest (ctx: MeasureContext, domain: OperationDomain, params: DomainParams): Promise { - return ctx.with( - `${domain}-${Object.keys(params)[0]}`, - {}, - (ctx) => this.provideDomainRequest(ctx, domain, params), - { - workspace: this.context.workspace.uuid - } - ) - } - - async tx (ctx: MeasureContext, txes: Tx[]): Promise { - let measureName: string | undefined - - const tx = txes.find((it) => it._class === core.class.TxApplyIf) - if (tx !== undefined) { - // We have at least one fineOne, lets' perform each TxApply individually - if ((tx as TxApplyIf).measureName !== undefined) { - measureName = (tx as TxApplyIf).measureName - } - } - - let op: OperationLog | undefined - let opLogMetrics: Metrics | undefined - - const result = await ctx.with( - 'client-tx', - measureName !== undefined - ? { measureName, source: ctx.contextData.service } - : { source: ctx.contextData.service }, - (ctx) => { - ;({ opLogMetrics, op } = registerOperationLog(ctx)) - return this.provideTx(ctx, txes) - } - ) - updateOperationLog(opLogMetrics, op) - - return result - } -} diff --git a/server/middleware/src/dbAdapter.ts b/server/middleware/src/dbAdapter.ts deleted file mode 100644 index a01ce85a53e..00000000000 --- a/server/middleware/src/dbAdapter.ts +++ /dev/null @@ -1,91 +0,0 @@ -// -// 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 { DOMAIN_MODEL_TX, DOMAIN_TX, withContext, type MeasureContext } from '@hcengineering/core' -import type { - DbAdapter, - DbConfiguration, - Middleware, - MiddlewareCreator, - PipelineContext, - TxAdapter -} from '@hcengineering/server-core' -import { BaseMiddleware, createServiceAdaptersManager, DbAdapterManagerImpl } from '@hcengineering/server-core' - -/** - * @public - */ -export class DBAdapterMiddleware extends BaseMiddleware implements Middleware { - constructor ( - context: PipelineContext, - next: Middleware | undefined, - readonly conf: DbConfiguration - ) { - super(context, next) - } - - static create (conf: DbConfiguration): MiddlewareCreator { - return async (ctx, context, next): Promise => { - const middleware = new DBAdapterMiddleware(context, next, conf) - await middleware.init(ctx) - return middleware - } - } - - @withContext('dbAdapter-middleware') - async init (ctx: MeasureContext): Promise { - const adapters = new Map() - - await ctx.with('create-adapters', {}, async (ctx) => { - for (const key in this.conf.adapters) { - const adapterConf = this.conf.adapters[key] - adapters.set( - key, - await adapterConf.factory( - ctx, - this.context.hierarchy, - adapterConf.url, - this.context.workspace, - this.context.modelDb, - this.context.storageAdapter - ) - ) - } - }) - - const metrics = ctx.newChild('📔 adapters', {}, { span: false }) - - const txAdapterName = this.conf.domains[DOMAIN_TX] - const txAdapter = adapters.get(txAdapterName) as TxAdapter - await txAdapter.init?.(metrics, this.context.contextVars, [DOMAIN_TX, DOMAIN_MODEL_TX]) - - const defaultAdapter = adapters.get(this.conf.defaultAdapter) - if (defaultAdapter === undefined) { - throw new Error(`No default Adapter for ${this.conf.defaultAdapter}`) - } - - this.context.serviceAdapterManager = await createServiceAdaptersManager(this.conf.serviceAdapters, metrics) - - // We need to init all next, since we will use model - - const adapterManager = new DbAdapterManagerImpl(metrics, this.conf, this.context, defaultAdapter, adapters) - this.context.adapterManager = adapterManager - } - - async close (): Promise { - await this.context.adapterManager?.close() - await this.context.serviceAdapterManager?.close() - } -} diff --git a/server/middleware/src/dbAdapterHelper.ts b/server/middleware/src/dbAdapterHelper.ts deleted file mode 100644 index f0decce8a48..00000000000 --- a/server/middleware/src/dbAdapterHelper.ts +++ /dev/null @@ -1,39 +0,0 @@ -// -// 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 { withContext, type MeasureContext } from '@hcengineering/core' -import type { Middleware, PipelineContext } from '@hcengineering/server-core' -import { BaseMiddleware, DomainIndexHelperImpl } from '@hcengineering/server-core' - -/** - * @public - */ -export class DBAdapterInitMiddleware extends BaseMiddleware implements Middleware { - @withContext('db-adapter-init') - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next?: Middleware - ): Promise { - await ctx.with('init-adapters', {}, async (ctx) => { - await context.adapterManager?.initAdapters?.(ctx) - }) - const domainHelper = new DomainIndexHelperImpl(ctx, context.hierarchy, context.modelDb, context.workspace.uuid) - await ctx.with('register-helper', {}, async (ctx) => { - await context.adapterManager?.registerHelper?.(ctx, domainHelper) - }) - return undefined - } -} diff --git a/server/middleware/src/derivedEntry.ts b/server/middleware/src/derivedEntry.ts deleted file mode 100644 index 3c3971b5a62..00000000000 --- a/server/middleware/src/derivedEntry.ts +++ /dev/null @@ -1,33 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type MeasureContext } from '@hcengineering/core' -import type { Middleware, PipelineContext } from '@hcengineering/server-core' -import { BaseMiddleware } from '@hcengineering/server-core' - -/** - * Will support apply tx - * @public - */ -export class MarkDerivedEntryMiddleware extends BaseMiddleware implements Middleware { - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next?: Middleware - ): Promise { - context.derived = next - return undefined - } -} diff --git a/server/middleware/src/domainFind.ts b/server/middleware/src/domainFind.ts deleted file mode 100644 index e70920448f2..00000000000 --- a/server/middleware/src/domainFind.ts +++ /dev/null @@ -1,88 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Class, - type Doc, - type DocumentQuery, - type Domain, - type FindOptions, - type FindResult, - type MeasureContext, - type Ref, - type SessionData, - DOMAIN_MODEL -} from '@hcengineering/core' -import { PlatformError, unknownError } from '@hcengineering/platform' -import type { DBAdapterManager, Middleware, PipelineContext, ServerFindOptions } from '@hcengineering/server-core' -import { BaseMiddleware } from '@hcengineering/server-core' -import { emptyFindResult } from '@hcengineering/server-core/src/base' - -/** - * Will perform a find inside adapters - * @public - */ -export class DomainFindMiddleware extends BaseMiddleware implements Middleware { - adapterManager!: DBAdapterManager - - static async create (ctx: MeasureContext, context: PipelineContext, next?: Middleware): Promise { - const middleware = new DomainFindMiddleware(context, next) - if (context.adapterManager == null) { - throw new PlatformError(unknownError('Adapter maneger should be configured')) - } - middleware.adapterManager = context.adapterManager - return middleware - } - - toPrintableOptions (options?: ServerFindOptions): FindOptions { - const { ctx, allowedSpaces, associations, ...opt } = options ?? {} - return opt - } - - findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: ServerFindOptions - ): Promise> { - if (query?.$search !== undefined) { - // Server storage pass $search queries to next - return this.next?.findAll(ctx, _class, query, options) ?? emptyFindResult - } - const p = options?.prefix ?? 'client' - const domain = this.context.hierarchy.getDomain(_class) - if (domain === DOMAIN_MODEL) { - return Promise.resolve(this.context.modelDb.findAllSync(_class, query, options)) - } - return ctx.with( - p + '-find-all', - { source: ctx.contextData?.service ?? 'system', _class }, - (ctx) => { - return this.adapterManager.getAdapter(domain, false).findAll(ctx, _class, query, options) - }, - { _class, query, options: this.toPrintableOptions(options) }, - { span: 'skip' } - ) - } - - groupBy( - ctx: MeasureContext, - domain: Domain, - field: string, - query?: DocumentQuery

- ): Promise> { - return this.adapterManager.getAdapter(domain, false).groupBy(ctx, domain, field, query) - } -} diff --git a/server/middleware/src/domainTx.ts b/server/middleware/src/domainTx.ts deleted file mode 100644 index 9fd60727b32..00000000000 --- a/server/middleware/src/domainTx.ts +++ /dev/null @@ -1,166 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type Domain, - DOMAIN_MODEL, - groupByArray, - TxProcessor, - withContext, - type Doc, - type MeasureContext, - type SessionData, - type Tx, - type TxCUD, - type TxResult -} from '@hcengineering/core' -import { PlatformError, unknownError } from '@hcengineering/platform' -import type { - DbAdapter, - DBAdapterManager, - Middleware, - PipelineContext, - TxMiddlewareResult -} from '@hcengineering/server-core' -import { BaseMiddleware } from '@hcengineering/server-core' - -/** - * Will route transactions to domain adapters. - * @public - */ -export class DomainTxMiddleware extends BaseMiddleware implements Middleware { - adapterManager!: DBAdapterManager - - @withContext('domainTx-middleware') - static async create (ctx: MeasureContext, context: PipelineContext, next?: Middleware): Promise { - const middleware = new DomainTxMiddleware(context, next) - if (context.adapterManager == null) { - throw new PlatformError(unknownError('Domain adapter manager should be specified')) - } - middleware.adapterManager = context.adapterManager - return middleware - } - - async tx (ctx: MeasureContext, txes: Tx[]): Promise { - const txToStore: Tx[] = [] - - for (const tx of txes) { - if (TxProcessor.isExtendsCUD(tx._class)) { - txToStore.push(tx) - } - } - let result: TxMiddlewareResult = {} - if (txToStore.length > 0) { - result = await this.routeTx(ctx, txToStore) - } - // Chain to next, to update all stuff - await this.provideTx(ctx, txes) - if (Array.isArray(result) && result.length === 1) { - return result[0] - } - return result - } - - private async routeTx (ctx: MeasureContext, txes: Tx[]): Promise { - const result: TxResult[] = [] - - const adapterGroups = new Map[]>() - - const routeToAdapter = async (adapter: DbAdapter, txes: TxCUD[]): Promise => { - if (txes.length > 0) { - // Find all deleted documents - const toDelete = txes.filter((it) => it._class === core.class.TxRemoveDoc) - - if (toDelete.length > 0) { - const deleteByDomain = groupByArray(toDelete, (it) => this.context.hierarchy.getDomain(it.objectClass)) - - for (const [domain, domainTxes] of deleteByDomain.entries()) { - if (domain === DOMAIN_MODEL) { - for (const tx of domainTxes) { - const ddoc = this.context.modelDb.findObject(tx.objectId) - if (ddoc !== undefined) { - ctx.contextData.removedMap.set(ddoc._id, ddoc) - } - } - } else { - const todel = await ctx.with( - 'adapter-load', - {}, - () => - adapter.load( - ctx, - domain, - domainTxes.map((it) => it.objectId) - ), - { count: toDelete.length } - ) - - for (const ddoc of todel) { - ctx.contextData.removedMap.set(ddoc._id, ddoc) - } - } - } - } - - const classes = Array.from(new Set(txes.map((it) => it.objectClass))) - const _classes = Array.from(new Set(txes.map((it) => it._class))) - const r = await ctx.with('adapter-tx', {}, (ctx) => adapter.tx(ctx, ...txes), { - txes: txes.length, - classes, - _classes - }) - - if (Array.isArray(r)) { - result.push(...r) - } else { - result.push(r) - } - } - } - - const domains = new Set() - for (const tx of txes) { - const txCUD = tx as TxCUD - if (!TxProcessor.isExtendsCUD(txCUD._class)) { - // Skip unsupported tx - ctx.error('Unsupported transaction', tx) - continue - } - const domain = this.context.hierarchy.findDomain(txCUD.objectClass) - if (domain === undefined) continue - domains.add(domain) - const adapterName = this.adapterManager.getAdapterName(domain) - - let group = adapterGroups.get(adapterName) - if (group === undefined) { - group = [] - adapterGroups.set(adapterName, group) - } - group.push(txCUD) - } - - // We need to mark domains to set existing - for (const d of domains) { - // We need to mark adapter - this.adapterManager.getAdapter(d, true) - } - - for (const [adapterName, txes] of adapterGroups.entries()) { - const adapter = this.adapterManager.getAdapterByName(adapterName, true) - await routeToAdapter(adapter, txes) - } - return result - } -} diff --git a/server/middleware/src/findSecurity.ts b/server/middleware/src/findSecurity.ts deleted file mode 100644 index c8fce0cb35b..00000000000 --- a/server/middleware/src/findSecurity.ts +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import { - type Class, - type Doc, - type DocumentQuery, - type FindOptions, - type FindResult, - type MeasureContext, - type Ref, - type SessionData -} from '@hcengineering/core' -import { BaseMiddleware, type Middleware, type PipelineContext } from '@hcengineering/server-core' - -/** - * @public - */ -export class FindSecurityMiddleware extends BaseMiddleware implements Middleware { - private constructor (context: PipelineContext, next?: Middleware) { - super(context, next) - } - - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new FindSecurityMiddleware(context, next) - } - - findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - if (options !== undefined) { - const { limit, sort, lookup, projection, associations, total, showArchived } = options - return this.provideFindAll(ctx, _class, query, { - limit, - sort, - lookup, - projection, - associations, - total, - showArchived - }) - } - return this.provideFindAll(ctx, _class, query, options) - } -} diff --git a/server/middleware/src/fulltext.ts b/server/middleware/src/fulltext.ts deleted file mode 100644 index d69d3f12c97..00000000000 --- a/server/middleware/src/fulltext.ts +++ /dev/null @@ -1,385 +0,0 @@ -// -// 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 { Analytics } from '@hcengineering/analytics' -import core, { - docKey, - isFullTextAttribute, - isIndexedAttribute, - toFindResult, - type AttachedDoc, - type Class, - type Collection, - type Doc, - type DocumentQuery, - type FindOptions, - type FindResult, - type FullTextSearchContext, - type MeasureContext, - type ObjQueryType, - type Ref, - type SearchOptions, - type SearchQuery, - type SearchResult -} from '@hcengineering/core' -import type { IndexedDoc, Middleware, MiddlewareCreator, PipelineContext } from '@hcengineering/server-core' -import { BaseMiddleware } from '@hcengineering/server-core' -import card from '@hcengineering/card' -/** - * @public - */ -export class FullTextMiddleware extends BaseMiddleware implements Middleware { - fulltextEndpoint: string - contexts = new Map>, FullTextSearchContext>() - - constructor ( - context: PipelineContext, - next: Middleware | undefined, - fulltextUrl: string, - readonly token: string - ) { - super(context, next) - const fulltextEndpoints = fulltextUrl.split(';').map((it) => it.trim()) - - const hash = this.hashWorkspace(context.workspace.uuid) - this.fulltextEndpoint = fulltextEndpoints[Math.abs(hash % fulltextEndpoints.length)] - } - - hashWorkspace (dbWorkspaceName: string): number { - return [...dbWorkspaceName].reduce((hash, c) => (Math.imul(31, hash) + c.charCodeAt(0)) | 0, 0) - } - - static create (url: string, token: string): MiddlewareCreator { - return async (ctx, context, next): Promise => { - const middleware = new FullTextMiddleware(context, next, url, token) - await middleware.init(ctx) - return middleware - } - } - - async init (ctx: MeasureContext): Promise { - this.contexts = new Map( - this.context.modelDb.findAllSync(core.class.FullTextSearchContext, {}).map((it) => [it.toClass, it]) - ) - } - - async search( - _classes: Ref>[], - query: DocumentQuery, - fullTextLimit: number - ): Promise { - try { - return await ( - await fetch(this.fulltextEndpoint + '/api/v1/search', { - method: 'PUT', - keepalive: true, - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - token: this.token, - workspace: this.context.workspace.uuid, - _classes, - query, - fullTextLimit - }) - }) - ).json() - } catch (err: any) { - if (err?.cause?.code === 'ECONNRESET' || err?.cause?.code === 'ECONNREFUSED') { - // TODO: We need to send event about indexing is complete after a while - return [] - } - Analytics.handleError(err) - return [] - } - } - - async findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - if (query?.$search === undefined) { - return await this.provideFindAll(ctx, _class, query, options) - } - - const { _id, $search, ...mainQuery } = query - if ($search === undefined) { - return toFindResult([]) - } - - const ids: Set> = new Set>() - const childIds: Set> = new Set>() - const baseClass = this.context.hierarchy.getBaseClass(_class) - let classes = this.context.hierarchy.getDescendants(baseClass).filter((it) => !this.context.hierarchy.isMixin(it)) - - const attrs = this.context.hierarchy.getAllAttributes(_class) - - // We need to filter all non indexed fields from query to make it work properly - const findQuery: DocumentQuery = { - $search: query.$search - } - - const childClasses = new Set>>() - try { - for (const [k, attr] of attrs) { - if (isFullTextAttribute(attr) || isIndexedAttribute(attr)) { - const vv = (query as any)[k] - if (vv != null) { - if ( - k === '_class' || - k === 'modifiedBy' || - k === 'modifiedOn' || - k === 'space' || - k === 'attachedTo' || - k === 'attachedToClass' - ) { - findQuery[k] = vv - } else { - const docKeyValue = docKey(attr.name, attr.attributeOf) - findQuery[docKeyValue] = vv - } - } - } - if (attr.type._class === core.class.Collection) { - // we need attached documents to be in classes - const coll = attr.type as Collection - const dsc = this.context.hierarchy.getDescendants(coll.of).filter((it) => !this.context.hierarchy.isMixin(it)) - for (const d of dsc) { - childClasses.add(d) - } - } - } - if (this.context.hierarchy.isDerived(baseClass, card.class.Card)) { - // Using Card as base class because messages are the same for any card subclass - childClasses.add(`${card.class.Card}%message` as Ref>) - } - } catch (err: any) { - Analytics.handleError(err) - } - - classes = classes.filter((it, idx, arr) => arr.indexOf(it) === idx) - - classes = classes.filter((it) => { - if (typeof query._class === 'object') { - if (query._class?.$in !== undefined) { - return query._class.$in.includes(it) - } - if (query._class?.$nin !== undefined) { - return !query._class.$nin.includes(it) - } - } - return true - }) - - const fullTextLimit = Math.min(5000, (options?.limit ?? 200) * 100) - - // Find main documents, not attached ones. - const { indexedDocMap } = await this.findDocuments(classes, findQuery, fullTextLimit, baseClass, ids) - - for (const c of classes) { - // We do not need to overlap - childClasses.delete(c) - } - - const childQuery: DocumentQuery = { - $search: findQuery.$search, - attachedToClass: { $in: classes } - } - if (findQuery.space !== undefined) { - childQuery.space = findQuery.space - } - const { childDocs, childIndexedDocMap } = - childClasses !== undefined && childClasses.size > 0 - ? await this.findChildDocuments(Array.from(childClasses), childQuery, fullTextLimit, baseClass, childIds) - : { - childDocs: [], - childIndexedDocMap: new Map() - } - - const scoreSearch: number | undefined = (options?.sort as any)?.['#score'] - - const resultIds = Array.from(this.getResultIds(ids, _id)) - const childResultIds = Array.from(this.getResultIds(childIds, _id)) - resultIds.push(...childResultIds) - - let result = - resultIds.length > 0 - ? await this.provideFindAll( - ctx, - _class, - { _id: { $in: Array.from(new Set(resultIds)) }, ...mainQuery }, - options - ) - : toFindResult([]) - - // Just assign scores based on idex - result.forEach((it) => { - const idDoc = indexedDocMap.get(it._id) ?? childIndexedDocMap.get(it._id) - const { _score } = idDoc - - const maxScore = childDocs.reduceRight((p, cur) => (p > cur.$score ? p : cur.$score), _score) - - it.$source = { - $score: maxScore - } - }) - if (scoreSearch !== undefined) { - result.sort((a, b) => scoreSearch * ((a.$source?.$score ?? 0) - (b.$source?.$score ?? 0))) - if (options?.limit !== undefined && options?.limit < result.length) { - result = toFindResult(result.slice(0, options?.limit), result.total) - } - } - return result - } - - private async findDocuments( - classes: Ref>[], - findQuery: DocumentQuery, - fullTextLimit: number, - baseClass: Ref>, - ids: Set> - ): Promise<{ docs: IndexedDoc[], indexedDocMap: Map, IndexedDoc> }> { - const docs = await this.search(classes, findQuery, fullTextLimit) - - const indexedDocMap = new Map, IndexedDoc>() - - for (const doc of docs) { - if ( - doc._class != null && - Array.isArray(doc._class) && - doc._class.some((cl) => this.context.hierarchy.isDerived(cl, baseClass)) - ) { - ids.add(doc.id) - indexedDocMap.set(doc.id, doc) - } - if ( - doc._class !== null && - !Array.isArray(doc._class) && - this.context.hierarchy.isDerived(doc._class, baseClass) - ) { - ids.add(doc.id) - indexedDocMap.set(doc.id, doc) - } - } - return { docs, indexedDocMap } - } - - private async findChildDocuments( - classes: Ref>[], - findQuery: DocumentQuery, - fullTextLimit: number, - baseClass: Ref>, - ids: Set> - ): Promise<{ childDocs: IndexedDoc[], childIndexedDocMap: Map, IndexedDoc> }> { - const childDocs = await this.search(classes, findQuery, fullTextLimit) - - const childIndexedDocMap = new Map, IndexedDoc>() - - for (const doc of childDocs) { - if (doc.attachedTo != null) { - if (doc.attachedToClass != null && this.context.hierarchy.isDerived(doc.attachedToClass, baseClass)) { - if (this.context.hierarchy.isDerived(doc.attachedToClass, baseClass)) { - ids.add(doc.attachedTo) - childIndexedDocMap.set(doc.attachedTo, doc) - } - } else { - ids.add(doc.attachedTo) - childIndexedDocMap.set(doc.attachedTo, doc) - } - } - } - return { childDocs, childIndexedDocMap } - } - - async searchFulltext (ctx: MeasureContext, query: SearchQuery, options: SearchOptions): Promise { - try { - return await ctx.with('full-text-search', {}, async (ctx) => { - return await ( - await fetch(this.fulltextEndpoint + '/api/v1/full-text-search', { - method: 'PUT', - keepalive: true, - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - workspace: this.context.workspace.uuid, - token: this.token, - query, - options - }) - }) - ).json() - }) - } catch (err: any) { - if (err?.cause?.code === 'ECONNRESET' || err?.cause?.code === 'ECONNREFUSED') { - // TODO: We need to send event about indexing is complete after a while - return { docs: [] } - } - Analytics.handleError(err) - return { docs: [] } - } - } - - async close (): Promise { - try { - await fetch(this.fulltextEndpoint + '/api/v1/close', { - method: 'PUT', - keepalive: true, - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - token: this.token - }) - }) - } catch (err: any) { - if (err?.cause?.code === 'ECONNRESET' || err?.cause?.code === 'ECONNREFUSED') { - return - } - Analytics.handleError(err) - } - } - - getResultIds (ids: Set>, _id: ObjQueryType> | undefined): Set> { - const result = new Set>() - if (_id !== undefined) { - if (typeof _id === 'string') { - if (ids.has(_id)) { - result.add(_id) - } - } else if (_id.$in !== undefined) { - for (const id of _id.$in) { - if (ids.has(id)) { - result.add(id) - } - } - } else if (_id.$nin !== undefined) { - for (const id of _id.$nin) { - ids.delete(id) - } - return ids - } else if (_id.$ne !== undefined) { - ids.delete(_id.$ne) - return ids - } - } else { - return ids - } - return result - } -} diff --git a/server/middleware/src/guestPermissions.ts b/server/middleware/src/guestPermissions.ts deleted file mode 100644 index 6ac5ecf8df9..00000000000 --- a/server/middleware/src/guestPermissions.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { - BaseMiddleware, - type Middleware, - type PipelineContext, - type TxMiddlewareResult -} from '@hcengineering/server-core' -import core, { - AccountRole, - type Doc, - hasAccountRole, - type MeasureContext, - type SessionData, - type Space, - type Tx, - type TxApplyIf, - type TxCUD, - TxProcessor, - type TxUpdateDoc -} from '@hcengineering/core' -import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' - -export class GuestPermissionsMiddleware extends BaseMiddleware implements Middleware { - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new GuestPermissionsMiddleware(context, next) - } - - async tx (ctx: MeasureContext, txes: Tx[]): Promise { - const account = ctx.contextData.account - if (hasAccountRole(account, AccountRole.User)) { - return await this.provideTx(ctx, txes) - } - - if (account.role === AccountRole.DocGuest || account.role === AccountRole.ReadOnlyGuest) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) - } - - for (const tx of txes) { - this.processTx(ctx, tx) - } - - return await this.provideTx(ctx, txes) - } - - private processTx (ctx: MeasureContext, tx: Tx): void { - const h = this.context.hierarchy - if (tx._class === core.class.TxApplyIf) { - const applyTx = tx as TxApplyIf - for (const t of applyTx.txes) { - this.processTx(ctx, t) - } - return - } - if (TxProcessor.isExtendsCUD(tx._class)) { - const cudTx = tx as TxCUD - const isSpace = h.isDerived(cudTx.objectClass, core.class.Space) - if (isSpace) { - if (this.isForbiddenSpaceTx(cudTx as TxCUD)) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) - } - } else if (cudTx.space !== core.space.DerivedTx && this.isForbiddenTx(cudTx)) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) - } - } - } - - private isForbiddenTx (tx: TxCUD): boolean { - if (tx._class === core.class.TxMixin) return false - return !this.hasMixinAccessLevel(tx) - } - - private isForbiddenSpaceTx (tx: TxCUD): boolean { - if (tx._class === core.class.TxRemoveDoc) return true - if (tx._class === core.class.TxCreateDoc) { - return !this.hasMixinAccessLevel(tx) - } - if (tx._class === core.class.TxUpdateDoc) { - const updateTx = tx as TxUpdateDoc - const ops = updateTx.operations - const keys = ['members', 'private', 'archived', 'owners', 'autoJoin'] - if (keys.some((key) => (ops as any)[key] !== undefined)) { - return true - } - if (ops.$push !== undefined || ops.$pull !== undefined) { - return true - } - } - return false - } - - private hasMixinAccessLevel (tx: TxCUD): boolean { - const h = this.context.hierarchy - const accessLevelMixin = h.classHierarchyMixin(tx.objectClass, core.mixin.TxAccessLevel) - if (accessLevelMixin === undefined) return false - if (tx._class === core.class.TxCreateDoc) { - return accessLevelMixin.createAccessLevel === AccountRole.Guest - } - if (tx._class === core.class.TxRemoveDoc) { - return accessLevelMixin.removeAccessLevel === AccountRole.Guest - } - if (tx._class === core.class.TxUpdateDoc) { - return accessLevelMixin.updateAccessLevel === AccountRole.Guest - } - return false - } -} diff --git a/server/middleware/src/identity.ts b/server/middleware/src/identity.ts deleted file mode 100644 index 308819ed903..00000000000 --- a/server/middleware/src/identity.ts +++ /dev/null @@ -1,76 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import { aiBotAccountEmail } from '@hcengineering/ai-bot' -import core, { - type MeasureContext, - type Tx, - systemAccountUuid, - type SessionData, - type TxApplyIf -} from '@hcengineering/core' -import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import { - BaseMiddleware, - type Middleware, - type TxMiddlewareResult, - type PipelineContext -} from '@hcengineering/server-core' - -/** - * @public - */ -export class IdentityMiddleware extends BaseMiddleware implements Middleware { - private constructor (context: PipelineContext, next?: Middleware) { - super(context, next) - } - - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new IdentityMiddleware(context, next) - } - - tx (ctx: MeasureContext, txes: Tx[]): Promise { - const account = ctx.contextData.account - - if (account.uuid === systemAccountUuid || account.fullSocialIds.some((it) => it.value === aiBotAccountEmail)) { - // TODO: We need to enhance allowed list in case of user service, on behalf of user activities. - - // We pass for system accounts and services. - return this.provideTx(ctx, txes) - } - function checkTx (tx: Tx): void { - const mxAccount = ctx.contextData.socialStringsToUsers.get(tx.modifiedBy)?.accontUuid - if (mxAccount === undefined || mxAccount !== account.uuid) { - throw new PlatformError( - new Status(Severity.ERROR, platform.status.AccountMismatch, { - account: account.uuid, - requiredAccount: mxAccount - }) - ) - } - } - for (const tx of txes) { - checkTx(tx) - if (tx._class === core.class.TxApplyIf) { - const atx = tx as TxApplyIf - atx.txes.forEach(checkTx) - } - } - return this.provideTx(ctx, txes) - } -} diff --git a/server/middleware/src/index.ts b/server/middleware/src/index.ts deleted file mode 100644 index 6ed9e98cfe1..00000000000 --- a/server/middleware/src/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -// -// 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. -// - -export * from './applyTx' -export * from './broadcast' -export * from './configuration' -export * from './contextName' -export * from './dbAdapter' -export * from './dbAdapterHelper' -export * from './derivedEntry' -export * from './domainFind' -export * from './domainTx' -export * from './fulltext' -export * from './liveQuery' -export * from './lookup' -export * from './lowLevel' -export * from './model' -export * from './modified' -export * from './private' -export * from './queryJoin' -export * from './guestPermissions' -export * from './spacePermissions' -export * from './spaceSecurity' -export * from './triggers' -export * from './txPush' -export * from './queue' -export * from './identity' -export * from './pluginConfig' -export * from './userStatus' -export * from './findSecurity' -export * from './normalizeTx' diff --git a/server/middleware/src/liveQuery.ts b/server/middleware/src/liveQuery.ts deleted file mode 100644 index f76d8dfda2d..00000000000 --- a/server/middleware/src/liveQuery.ts +++ /dev/null @@ -1,98 +0,0 @@ -// -// Copyright © 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Client, - type Doc, - type Hierarchy, - type MeasureContext, - type ModelDb, - type SearchOptions, - type SearchQuery, - type Tx, - toFindResult -} from '@hcengineering/core' -import { LiveQuery as LQ } from '@hcengineering/query' -import { BaseMiddleware } from '@hcengineering/server-core' -import type { Middleware, PipelineContext, ServerFindOptions, TxMiddlewareResult } from '@hcengineering/server-core' - -/** - * @public - */ -export class LiveQueryMiddleware extends BaseMiddleware implements Middleware { - liveQuery: LQ - - constructor (metrics: MeasureContext, context: PipelineContext, next?: Middleware) { - super(context, next) - this.liveQuery = new LQ(this.newCastClient(context.hierarchy, context.modelDb, metrics)) - this.context.liveQuery = this.liveQuery - } - - private newCastClient (hierarchy: Hierarchy, modelDb: ModelDb, metrics: MeasureContext): Client { - return { - getHierarchy (): Hierarchy { - return hierarchy - }, - getModel (): ModelDb { - return modelDb - }, - close: () => Promise.resolve(), - findAll: async (_class, query, options) => { - const _ctx: MeasureContext = (options as ServerFindOptions)?.ctx ?? metrics - delete (options as ServerFindOptions)?.ctx - - const results = await this.findAll(_ctx, _class, query, options) - return toFindResult( - results.map((v) => { - return this.context.hierarchy.updateLookupMixin(_class, v, options) - }), - results.total - ) - }, - findOne: async (_class, query, options) => { - const _ctx: MeasureContext = (options as ServerFindOptions)?.ctx ?? metrics - delete (options as ServerFindOptions)?.ctx - - const results = await this.findAll(_ctx, _class, query, { ...options, limit: 1 }) - return toFindResult( - results.map((v) => { - return this.context.hierarchy.updateLookupMixin(_class, v, options) - }), - results.total - )[0] - }, - domainRequest: async (domain, params) => { - return await this.provideDomainRequest(metrics, domain, params) - }, - tx: (tx) => { - return Promise.resolve({}) - }, - searchFulltext: async (query: SearchQuery, options: SearchOptions) => { - // Cast client doesn't support fulltext search - return { docs: [] } - } - } - } - - static async create (ctx: MeasureContext, context: PipelineContext, next?: Middleware): Promise { - // we need to init triggers from model first. - return new LiveQueryMiddleware(ctx, context, next) - } - - async tx (ctx: MeasureContext, tx: Tx[]): Promise { - await this.liveQuery.tx(...tx) - return await this.provideTx(ctx, tx) - } -} diff --git a/server/middleware/src/lookup.ts b/server/middleware/src/lookup.ts deleted file mode 100644 index 7b5f1da4adc..00000000000 --- a/server/middleware/src/lookup.ts +++ /dev/null @@ -1,118 +0,0 @@ -// -// Copyright © 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Class, - type Doc, - type DocumentQuery, - type FindOptions, - type FindResult, - type MeasureContext, - type Ref, - clone, - toFindResult -} from '@hcengineering/core' -import { BaseMiddleware, type Middleware, type PipelineContext } from '@hcengineering/server-core' -/** - * @public - */ -export class LookupMiddleware extends BaseMiddleware implements Middleware { - private constructor (context: PipelineContext, next?: Middleware) { - super(context, next) - } - - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new LookupMiddleware(context, next) - } - - override async findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - const result = await this.provideFindAll(ctx, _class, query, options) - // Fill lookup map to make more compact representation - - if (options?.lookup !== undefined) { - const newResult: T[] = [] - let counter = 0 - const idClassMap: Record = {} - - function mapDoc (doc: Doc): number { - const key = doc._class + '@' + doc._id - let docRef = idClassMap[key] - if (docRef === undefined) { - docRef = { id: ++counter, doc, count: -1 } - idClassMap[key] = docRef - } - docRef.count++ - return docRef.id - } - - for (const d of result) { - const newDoc: any = { ...d } - if (d.$lookup !== undefined) { - newDoc.$lookup = clone(d.$lookup) - newResult.push(newDoc) - for (const [k, v] of Object.entries(d.$lookup)) { - if (!Array.isArray(v)) { - newDoc.$lookup[k] = v != null ? mapDoc(v) : v - } else { - newDoc.$lookup[k] = v.map((it) => (it != null ? mapDoc(it) : it)) - } - } - } else { - newResult.push(newDoc) - } - } - const lookupMap = Object.fromEntries(Array.from(Object.values(idClassMap)).map((it) => [it.id, it.doc])) - return this.cleanQuery(toFindResult(newResult, result.total, lookupMap), query, lookupMap) - } - - // We need to get rid of simple query parameters matched in documents - return this.cleanQuery(result, query) - } - - private cleanQuery( - result: FindResult, - query: DocumentQuery, - lookupMap?: Record - ): FindResult { - const newResult: T[] = [] - for (const doc of result) { - let _doc = doc - let cloned = false - for (const [k, v] of Object.entries(query)) { - if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') { - if ((_doc as any)[k] === v) { - if (!cloned) { - _doc = { ...doc } as any - cloned = true - } - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete (_doc as any)[k] - } - } - } - newResult.push(_doc) - } - return toFindResult(newResult, result.total, lookupMap) - } -} diff --git a/server/middleware/src/lowLevel.ts b/server/middleware/src/lowLevel.ts deleted file mode 100644 index 51ac86472b5..00000000000 --- a/server/middleware/src/lowLevel.ts +++ /dev/null @@ -1,91 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type DocumentQuery, - type DocumentUpdate, - type FindOptions, - type Doc, - type Domain, - type Iterator, - type MeasureContext, - type Ref, - type StorageIterator -} from '@hcengineering/core' -import { PlatformError, unknownStatus } from '@hcengineering/platform' -import type { Middleware, PipelineContext } from '@hcengineering/server-core' -import { BaseMiddleware } from '@hcengineering/server-core' - -/** - * Will perform a find inside adapters - * @public - */ -export class LowLevelMiddleware extends BaseMiddleware implements Middleware { - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - if (context.adapterManager == null) { - throw new PlatformError(unknownStatus('No AdapterManager')) - } - const adapterManager = context.adapterManager - context.lowLevelStorage = { - find (ctx: MeasureContext, domain: Domain): StorageIterator { - return adapterManager.getAdapter(domain, false).find(ctx, domain) - }, - - load (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return adapterManager.getAdapter(domain, false).load(ctx, domain, docs) - }, - - upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise { - return adapterManager.getAdapter(domain, true).upload(ctx, domain, docs) - }, - - clean (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return adapterManager.getAdapter(domain, true).clean(ctx, domain, docs) - }, - groupBy( - ctx: MeasureContext, - domain: Domain, - field: string, - query?: DocumentQuery

- ): Promise> { - return adapterManager.getAdapter(domain, false).groupBy(ctx, domain, field, query) - }, - rawFindAll(domain: Domain, query: DocumentQuery, options?: FindOptions): Promise { - return adapterManager.getAdapter(domain, false).rawFindAll(domain, query, options) - }, - rawUpdate(domain: Domain, query: DocumentQuery, operations: DocumentUpdate): Promise { - return adapterManager.getAdapter(domain, true).rawUpdate(domain, query, operations) - }, - rawDeleteMany (domain, query) { - return adapterManager.getAdapter(domain, true).rawDeleteMany(domain, query) - }, - getDomainHash (ctx: MeasureContext, domain: Domain): Promise { - return adapterManager.getAdapter(domain, false).getDomainHash(ctx, domain) - }, - traverse( - domain: Domain, - query: DocumentQuery, - options?: Pick, 'sort' | 'limit' | 'projection'> - ): Promise> { - return adapterManager.getAdapter(domain, false).traverse(domain, query, options) - } - } - return undefined - } -} diff --git a/server/middleware/src/model.ts b/server/middleware/src/model.ts deleted file mode 100644 index f6bd24ced01..00000000000 --- a/server/middleware/src/model.ts +++ /dev/null @@ -1,179 +0,0 @@ -// -// 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 core, { - type Class, - type Doc, - type DocumentQuery, - type FindOptions, - type FindResult, - type Hierarchy, - type LoadModelResponse, - type MeasureContext, - type Ref, - type SessionData, - type Timestamp, - type Tx, - type TxCUD, - DOMAIN_MODEL, - DOMAIN_TX, - withContext -} from '@hcengineering/core' -import { PlatformError, unknownError } from '@hcengineering/platform' -import type { - Middleware, - MiddlewareCreator, - PipelineContext, - TxAdapter, - TxMiddlewareResult -} from '@hcengineering/server-core' -import { BaseMiddleware } from '@hcengineering/server-core' -import crypto from 'node:crypto' - -const isAccountTx = (it: TxCUD): boolean => - ['core:class:Account', 'contact:class:PersonAccount'].includes(it.objectClass) - -/** - * @public - */ -export class ModelMiddleware extends BaseMiddleware implements Middleware { - lastHash: string = '' - lastHashResponse!: Promise - - constructor ( - context: PipelineContext, - next: Middleware | undefined, - readonly systemTx: Tx[], - readonly filter?: (h: Hierarchy, model: Tx[]) => Tx[] - ) { - super(context, next) - } - - @withContext('modelAdapter-middleware') - static async doCreate ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined, - systemTx: Tx[], - filter?: (h: Hierarchy, model: Tx[]) => Tx[] - ): Promise { - const middleware = new ModelMiddleware(context, next, systemTx, filter) - await middleware.init(ctx) - return middleware - } - - static create (tx: Tx[], filter?: (h: Hierarchy, model: Tx[]) => Tx[]): MiddlewareCreator { - return (ctx, context, next) => { - return this.doCreate(ctx, context, next, tx, filter) - } - } - - @withContext('get-model') - async getUserTx (ctx: MeasureContext, txAdapter: TxAdapter): Promise { - const allUserTxes = await ctx.with('fetch-model', {}, (ctx) => txAdapter.getModel(ctx)) - return allUserTxes.filter((it) => !isAccountTx(it as TxCUD)) - } - - findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - const d = this.context.hierarchy.findDomain(_class) - if (d === DOMAIN_MODEL) { - return this.context.modelDb.findAll(_class, query, options) - } - return this.provideFindAll(ctx, _class, query, options) - } - - async init (ctx: MeasureContext): Promise { - if (this.context.adapterManager == null) { - throw new PlatformError(unknownError('Adapter manager should be configured')) - } - const txAdapter = this.context.adapterManager.getAdapter(DOMAIN_TX, true) as TxAdapter - - const userTx = await this.getUserTx(ctx, txAdapter) - const model = this.systemTx.concat(userTx) - for (const tx of model) { - try { - this.context.hierarchy.tx(tx) - } catch (err: any) { - ctx.warn('failed to apply model transaction, skipping', { tx: JSON.stringify(tx), err }) - } - } - const fmodel = this.filter !== undefined ? this.filter(this.context.hierarchy, model) : model - this.context.modelDb.addTxes(ctx, fmodel, true) - - this.setModel(fmodel) - } - - private addModelTx (tx: Tx): void { - const h = crypto.createHash('sha1') - h.update(this.lastHash) - h.update(JSON.stringify(tx)) - const hash = h.digest('hex') - this.setLastHash(hash) - } - - private setLastHash (hash: string): void { - this.lastHash = hash - this.context.lastHash = this.lastHash - } - - private setModel (model: Tx[]): void { - let last = '' - model.map((it, index) => { - const h = crypto.createHash('sha1') - h.update(last) - h.update(JSON.stringify(it)) - last = h.digest('hex') - return [last, index] - }) - this.setLastHash(last) - } - - async loadModel (ctx: MeasureContext, lastModelTx: Timestamp, hash?: string): Promise { - if (hash !== undefined) { - if (hash === this.lastHash) { - return { - full: false, - hash, - transactions: [] - } - } - const txAdapter = this.context.adapterManager?.getAdapter(DOMAIN_TX, true) as TxAdapter - return { - full: true, - hash: this.lastHash, - transactions: this.systemTx.concat(await this.getUserTx(ctx, txAdapter)) - } - } - const txAdapter = this.context.adapterManager?.getAdapter(DOMAIN_TX, true) as TxAdapter - return this.systemTx.concat(await this.getUserTx(ctx, txAdapter)).filter((it) => it.modifiedOn > lastModelTx) - } - - tx (ctx: MeasureContext, tx: Tx[]): Promise { - const modelTxes = tx.filter((it) => it.objectSpace === core.space.Model) - if (modelTxes.length > 0) { - for (const t of modelTxes) { - this.context.hierarchy.tx(t) - this.addModelTx(t) - } - this.context.modelDb.addTxes(ctx, modelTxes, true) - } - return this.provideTx(ctx, tx) - } -} diff --git a/server/middleware/src/modified.ts b/server/middleware/src/modified.ts deleted file mode 100644 index 15ef0d80b09..00000000000 --- a/server/middleware/src/modified.ts +++ /dev/null @@ -1,62 +0,0 @@ -// -// 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 core, { - type MeasureContext, - type Tx, - systemAccountUuid, - type SessionData, - type TxApplyIf -} from '@hcengineering/core' -import { - BaseMiddleware, - type Middleware, - type TxMiddlewareResult, - type PipelineContext -} from '@hcengineering/server-core' - -/** - * @public - */ -export class ModifiedMiddleware extends BaseMiddleware implements Middleware { - private constructor (context: PipelineContext, next?: Middleware) { - super(context, next) - } - - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new ModifiedMiddleware(context, next) - } - - tx (ctx: MeasureContext, txes: Tx[]): Promise { - const now = Date.now() - function updateTx (tx: Tx): void { - if (tx.modifiedBy !== core.account.System && ctx.contextData.account.uuid !== systemAccountUuid) { - tx.modifiedOn = now - tx.createdOn = tx.modifiedOn - } - } - for (const tx of txes) { - updateTx(tx) - if (tx._class === core.class.TxApplyIf) { - const atx = tx as TxApplyIf - atx.txes.forEach(updateTx) - } - } - return this.provideTx(ctx, txes) - } -} diff --git a/server/middleware/src/normalizeTx.ts b/server/middleware/src/normalizeTx.ts deleted file mode 100644 index 26d241599c9..00000000000 --- a/server/middleware/src/normalizeTx.ts +++ /dev/null @@ -1,259 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import core, { - type MeasureContext, - type Tx, - type SessionData, - type TxApplyIf, - type Ref, - type Class, - type Doc, - type Space, - type PersonId, - TxProcessor, - type TxWorkspaceEvent, - type OperationDomain, - type TxDomainEvent, - type TxCUD, - type TxModelUpgrade, - type TxCreateDoc, - type TxUpdateDoc, - type TxRemoveDoc, - type TxMixin, - type Mixin -} from '@hcengineering/core' -import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import { - BaseMiddleware, - type Middleware, - type TxMiddlewareResult, - type PipelineContext -} from '@hcengineering/server-core' - -// Helper types to require update in validation after Tx types are changed -type ExplicitUndefined = { [P in keyof Required]: Exclude | Extract } -type WithIdType = Omit & { _id: I } -type ExplicitTx = WithIdType, Ref> - -/** - * Validates properties in known Tx interfaces and removes unrecognized properties - * @public - */ -export class NormalizeTxMiddleware extends BaseMiddleware implements Middleware { - private constructor (context: PipelineContext, next?: Middleware) { - super(context, next) - } - - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new NormalizeTxMiddleware(context, next) - } - - tx (ctx: MeasureContext, txes: unknown[]): Promise { - const parsedTxes = [] - for (const tx of txes) { - const parsedTx = this.parseTx(tx) - if (parsedTx === undefined) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})) - } - parsedTxes.push(parsedTx) - } - return this.provideTx(ctx, parsedTxes) - } - - private checkMeta (meta: unknown): meta is Record | undefined { - if (meta === undefined) { - return true - } - if (meta === null || typeof meta !== 'object') { - return false - } - for (const [, val] of Object.entries(meta)) { - if (typeof val !== 'string' && typeof val !== 'number' && typeof val !== 'boolean') { - return false - } - } - return true - } - - private parseBaseTx (source: unknown): ExplicitTx | undefined { - if (source == null || typeof source !== 'object') { - return undefined - } - const { _class, _id, space, modifiedBy, modifiedOn, createdBy, createdOn, objectSpace, meta } = source as Record< - keyof Tx, - unknown - > - const isTxValid = - typeof _class === 'string' && - typeof _id === 'string' && - typeof space === 'string' && - typeof modifiedBy === 'string' && - typeof modifiedOn === 'number' && - (createdBy === undefined || typeof createdBy === 'string') && - (createdOn === undefined || typeof createdOn === 'number') && - typeof objectSpace === 'string' && - this.checkMeta(meta) - if (!isTxValid) { - return undefined - } - const baseTx: ExplicitTx = { - _class: _class as Ref>, - _id: _id as Ref, - space: space as Ref, - modifiedBy: modifiedBy as PersonId, - modifiedOn, - createdBy: createdBy as PersonId | undefined, - createdOn, - objectSpace: objectSpace as Ref, - meta: meta as Record | undefined - } - return baseTx - } - - private parseTx (source: unknown): Tx | undefined { - const baseTx = this.parseBaseTx(source) - if (baseTx === undefined) { - return undefined - } - - if (TxProcessor.isExtendsCUD(baseTx._class)) { - return this.parseTxCUD(source, baseTx) - } else if (baseTx._class === core.class.TxWorkspaceEvent) { - const { event, params } = source as any - if (typeof event !== 'number') { - return undefined - } - const workspaceEvent: ExplicitTx = Object.assign(baseTx, { - event, - params - }) - return workspaceEvent - } else if (baseTx._class === core.class.TxDomainEvent) { - const { domain, event } = source as any - if (typeof domain !== 'string') { - return undefined - } - const domainEvent: ExplicitTx = Object.assign(baseTx, { - domain: domain as OperationDomain, - event - }) - return domainEvent - } else if (baseTx._class === core.class.TxApplyIf) { - const { scope, match, notMatch, txes, notify, extraNotify, measureName } = source as Record< - keyof TxApplyIf, - unknown - > - const isValid = - (scope === undefined || typeof scope === 'string') && - (match === undefined || Array.isArray(match)) && - (notMatch === undefined || Array.isArray(notMatch)) && - Array.isArray(txes) && - (notify === undefined || typeof notify === 'boolean') && - (extraNotify === undefined || Array.isArray(extraNotify)) && - (measureName === undefined || typeof measureName === 'string') - if (!isValid) { - return undefined - } - const parsedTxes: TxCUD[] = [] - for (const tx of txes) { - const baseChildTx = this.parseBaseTx(tx) - if (baseChildTx === undefined || !TxProcessor.isExtendsCUD(baseChildTx._class)) { - return undefined - } - const parsed = this.parseTxCUD(tx, baseChildTx) - if (parsed === undefined) { - return undefined - } - parsedTxes.push(parsed as TxCUD) - } - const applyIf: ExplicitTx = Object.assign(baseTx, { - scope, - match, - notMatch, - txes: parsedTxes, - notify, - extraNotify, - measureName - }) - return applyIf - } else if (baseTx._class === core.class.TxModelUpgrade) { - const modelUpgrade: TxModelUpgrade = baseTx - return modelUpgrade - } - - return undefined - } - - private parseTxCUD (source: unknown, base: ExplicitTx): ExplicitTx> | undefined { - const { objectId, objectClass, attachedTo, attachedToClass, collection } = source as Record< - keyof TxCUD, - unknown - > - const isValid = - typeof objectId === 'string' && - typeof objectClass === 'string' && - (attachedTo === undefined || typeof attachedTo === 'string') && - (attachedToClass === undefined || typeof attachedToClass === 'string') && - (collection === undefined || typeof collection === 'string') - if (!isValid) { - return undefined - } - const baseCUD: ExplicitTx> = Object.assign(base, { - objectId: objectId as Ref, - objectClass: objectClass as Ref>, - attachedTo: attachedTo as Ref, - attachedToClass: attachedToClass as Ref>, - collection - }) - if (baseCUD._class === core.class.TxCreateDoc) { - const { attributes } = source as Record, unknown> - if (typeof attributes !== 'object' || attributes === null) { - return undefined - } - const createDoc: ExplicitTx> = Object.assign(baseCUD, { attributes }) - return createDoc - } else if (baseCUD._class === core.class.TxUpdateDoc) { - const { operations, retrieve } = source as Record, unknown> - if ( - typeof operations !== 'object' || - operations === null || - (retrieve !== undefined && typeof retrieve !== 'boolean') - ) { - return undefined - } - const updateDoc: ExplicitTx> = Object.assign(baseCUD, { operations, retrieve }) - return updateDoc - } else if (baseCUD._class === core.class.TxRemoveDoc) { - const removeDoc: ExplicitTx> = baseCUD - return removeDoc - } else if (baseCUD._class === core.class.TxMixin) { - const { mixin, attributes } = source as Record, unknown> - if (typeof mixin !== 'string' || typeof attributes !== 'object' || attributes === null) { - return undefined - } - const txMixin: ExplicitTx> = Object.assign(baseCUD, { - mixin: mixin as Ref>, - attributes - }) - return txMixin - } else { - return undefined - } - } -} diff --git a/server/middleware/src/pluginConfig.ts b/server/middleware/src/pluginConfig.ts deleted file mode 100644 index 9faa4bc2648..00000000000 --- a/server/middleware/src/pluginConfig.ts +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import { aiBotAccountEmail } from '@hcengineering/ai-bot' -import core, { - AccountRole, - type MeasureContext, - type Tx, - TxProcessor, - systemAccountUuid, - type Doc, - type SessionData, - type TxApplyIf, - type TxCUD -} from '@hcengineering/core' -import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import { - BaseMiddleware, - type Middleware, - type TxMiddlewareResult, - type PipelineContext -} from '@hcengineering/server-core' - -/** - * @public - */ -export class PluginConfigurationMiddleware extends BaseMiddleware implements Middleware { - private constructor (context: PipelineContext, next?: Middleware) { - super(context, next) - } - - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new PluginConfigurationMiddleware(context, next) - } - - tx (ctx: MeasureContext, txes: Tx[]): Promise { - const account = ctx.contextData.account - if (account.uuid === systemAccountUuid || account.fullSocialIds.some((it) => it.value === aiBotAccountEmail)) { - // We pass for system accounts and services. - return this.provideTx(ctx, txes) - } - function checkTx (tx: Tx): void { - if (TxProcessor.isExtendsCUD(tx._class)) { - const cud = tx as TxCUD - if (cud.objectClass === core.class.PluginConfiguration && ctx.contextData.account.role !== AccountRole.Owner) { - throw new PlatformError( - new Status(Severity.ERROR, platform.status.Forbidden, { - account: account.uuid - }) - ) - } - } - } - for (const tx of txes) { - checkTx(tx) - if (tx._class === core.class.TxApplyIf) { - const atx = tx as TxApplyIf - atx.txes.forEach(checkTx) - } - } - return this.provideTx(ctx, txes) - } -} diff --git a/server/middleware/src/private.ts b/server/middleware/src/private.ts deleted file mode 100644 index ee02769bc6d..00000000000 --- a/server/middleware/src/private.ts +++ /dev/null @@ -1,171 +0,0 @@ -// -// 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. -// -/* eslint-disable @typescript-eslint/no-unused-vars */ -import core, { - type AttachedDoc, - type Class, - DOMAIN_TX, - type Doc, - type DocumentQuery, - type FindOptions, - type FindResult, - type LookupData, - type MeasureContext, - type Ref, - type Tx, - type TxCUD, - TxProcessor, - systemAccountUuid, - type SessionData, - AccountRole -} from '@hcengineering/core' -import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import { - BaseMiddleware, - type Middleware, - type TxMiddlewareResult, - type PipelineContext -} from '@hcengineering/server-core' -import { DOMAIN_PREFERENCE } from '@hcengineering/server-preference' - -/** - * @public - */ -export class PrivateMiddleware extends BaseMiddleware implements Middleware { - private readonly targetDomains = [DOMAIN_PREFERENCE] - - private constructor (context: PipelineContext, next?: Middleware) { - super(context, next) - } - - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new PrivateMiddleware(context, next) - } - - isTargetDomain (tx: Tx): boolean { - if (TxProcessor.isExtendsCUD(tx._class)) { - const txCUD = tx as TxCUD - const domain = this.context.hierarchy.getDomain(txCUD.objectClass) - return this.targetDomains.includes(domain) - } - return false - } - - tx (ctx: MeasureContext, txes: Tx[]): Promise { - for (const tx of txes) { - if (this.isTargetDomain(tx)) { - const account = ctx.contextData.account - if ( - !account.socialIds.includes(tx.modifiedBy) && - account.uuid !== systemAccountUuid && - account.role !== AccountRole.Owner - ) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) - } - const modifiedByAccount = ctx.contextData.socialStringsToUsers.get(tx.modifiedBy)?.accontUuid - const target = [account.uuid, systemAccountUuid] - if (modifiedByAccount !== undefined && !target.includes(modifiedByAccount)) { - target.push(modifiedByAccount) - } - ctx.contextData.broadcast.targets['checkDomain' + account.uuid] = async (tx) => { - if (this.isTargetDomain(tx)) { - return { - target - } - } - } - } - } - return this.provideTx(ctx, txes) - } - - override async findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - let newQuery = query - const domain = this.context.hierarchy.getDomain(_class) - if (this.targetDomains.includes(domain)) { - const account = ctx.contextData.account - const socialStrings = account.socialIds - if (account.uuid !== systemAccountUuid) { - newQuery = { - ...query, - createdBy: { $in: socialStrings } - } - } - } - const findResult = await this.provideFindAll(ctx, _class, newQuery, options) - if (domain === DOMAIN_TX) { - const account = ctx.contextData.account - const socialStrings = account.socialIds - if (account.uuid !== systemAccountUuid) { - const targetClasses = new Set( - this.context.hierarchy.getDescendants(core.class.Doc).filter((p) => { - const domain = this.context.hierarchy.findDomain(p) - return domain != null && this.targetDomains.includes(domain) - }) - ) - ;(findResult as FindResult as FindResult).filter( - (p) => - !TxProcessor.isExtendsCUD(p._class) || - !targetClasses.has((p as TxCUD).objectClass) || - (p.createdBy !== undefined && socialStrings.includes(p.createdBy)) - ) - } - } - if (options?.lookup !== undefined) { - for (const object of findResult) { - if (object.$lookup !== undefined) { - this.filterLookup(ctx, object.$lookup) - } - } - } - return findResult - } - - isAvailable (ctx: MeasureContext, doc: Doc): boolean { - const domain = this.context.hierarchy.getDomain(doc._class) - if (!this.targetDomains.includes(domain)) return true - const account = ctx.contextData.account - const socialStrings = account.socialIds - return (doc.createdBy !== undefined && socialStrings.includes(doc.createdBy)) || account.uuid === systemAccountUuid - } - - filterLookup(ctx: MeasureContext, lookup: LookupData): void { - for (const key in lookup) { - const val = lookup[key] - if (Array.isArray(val)) { - const arr: AttachedDoc[] = [] - for (const value of val) { - if (this.isAvailable(ctx, value)) { - arr.push(value) - } - } - lookup[key] = arr as any - } else if (val !== undefined) { - if (!this.isAvailable(ctx, val)) { - lookup[key] = undefined - } - } - } - } -} diff --git a/server/middleware/src/queryJoin.ts b/server/middleware/src/queryJoin.ts deleted file mode 100644 index 64df144fc0c..00000000000 --- a/server/middleware/src/queryJoin.ts +++ /dev/null @@ -1,157 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Class, - type Doc, - type DocumentQuery, - type Domain, - type FindResult, - type LoadModelResponse, - type MeasureContext, - type Ref, - type SearchOptions, - type SearchQuery, - type SearchResult, - type SessionData, - type Timestamp, - type Tx -} from '@hcengineering/core' -import { - BaseMiddleware, - type Middleware, - type PipelineContext, - type ServerFindOptions -} from '@hcengineering/server-core' - -interface Query { - key: string - result: object | Promise | undefined - callbacks: number - max: number -} -/** - * @public - */ -export class QueryJoiner { - private readonly queries: Map = new Map() - - async query(ctx: MeasureContext, key: string, retrieve: (ctx: MeasureContext) => Promise): Promise { - // Will find a query or add + 1 to callbacks - const q = this.getQuery(key) - try { - if (q.result === undefined) { - q.result = retrieve(ctx) - } - if (q.result instanceof Promise) { - q.result = await q.result - } - - return q.result as T - } finally { - q.callbacks-- - - this.removeFromQueue(q) - } - } - - private getQuery (key: string): Query { - const query = this.queries.get(key) - if (query === undefined) { - const q: Query = { - key, - result: undefined, - callbacks: 1, - max: 1 - } - this.queries.set(key, q) - return q - } - - query.callbacks++ - query.max++ - return query - } - - private removeFromQueue (q: Query): void { - if (q.callbacks === 0) { - this.queries.delete(q.key) - } - } -} - -/** - * @public - */ -export class QueryJoinMiddleware extends BaseMiddleware implements Middleware { - private readonly joiner: QueryJoiner - - private constructor (context: PipelineContext, next?: Middleware) { - super(context, next) - this.joiner = new QueryJoiner() - } - - loadModel ( - ctx: MeasureContext, - lastModelTx: Timestamp, - hash?: string - ): Promise { - return this.joiner.query(ctx, `model-${lastModelTx}${hash ?? ''}`, async (ctx) => { - return await this.provideLoadModel(ctx, lastModelTx, hash) - }) - } - - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new QueryJoinMiddleware(context, next) - } - - override findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: ServerFindOptions - ): Promise> { - const opt = { ...options } - delete opt.ctx - return this.joiner.query( - ctx, - `findAll-${_class}-${JSON.stringify(query)}-${JSON.stringify(options)}`, - async (ctx) => { - return await this.provideFindAll(ctx, _class, query, options) - } - ) - } - - groupBy( - ctx: MeasureContext, - domain: Domain, - field: string, - query?: DocumentQuery

- ): Promise> { - return this.joiner.query(ctx, `groupBy-${domain}-${field}-${JSON.stringify(query ?? {})})`, async (ctx) => { - return await this.provideGroupBy(ctx, domain, field, query) - }) - } - - searchFulltext (ctx: MeasureContext, query: SearchQuery, options: SearchOptions): Promise { - return this.joiner.query(ctx, `searchFulltext-${JSON.stringify(query)}-${JSON.stringify(options)}`, async (ctx) => { - return await this.provideSearchFulltext(ctx, query, options) - }) - } -} diff --git a/server/middleware/src/queue.ts b/server/middleware/src/queue.ts deleted file mode 100644 index c2741cc6a86..00000000000 --- a/server/middleware/src/queue.ts +++ /dev/null @@ -1,65 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type MeasureContext, type SessionData, type Tx } from '@hcengineering/core' -import { - BaseMiddleware, - type Middleware, - type MiddlewareCreator, - type PipelineContext, - type PlatformQueue, - type PlatformQueueProducer, - QueueTopic -} from '@hcengineering/server-core' - -export class QueueMiddleware extends BaseMiddleware { - txProducer: PlatformQueueProducer - connected: Promise | undefined - constructor ( - ctx: MeasureContext, - readonly context: PipelineContext, - protected readonly next: Middleware | undefined, - readonly queue: PlatformQueue - ) { - super(context, next) - this.txProducer = queue.getProducer(ctx, QueueTopic.Tx) - } - - static create (queue: PlatformQueue): MiddlewareCreator { - return async (ctx, context, next) => { - return new QueueMiddleware(ctx, context, next, queue) - } - } - - async handleBroadcast (ctx: MeasureContext): Promise { - if (this.connected !== undefined) { - await this.connected - this.connected = undefined - } - - const meta = ctx.extractMeta() - - await Promise.all([ - this.provideBroadcast(ctx), - this.txProducer.send( - ctx, - this.context.workspace.uuid, - ctx.contextData.broadcast.txes - .concat(ctx.contextData.broadcast.queue) - .map((tx) => ({ ...tx, meta: { ...(tx.meta ?? {}), ...meta } })) - ) - ]) - } -} diff --git a/server/middleware/src/spacePermissions.ts b/server/middleware/src/spacePermissions.ts deleted file mode 100644 index 5b185c8f3c8..00000000000 --- a/server/middleware/src/spacePermissions.ts +++ /dev/null @@ -1,385 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import core, { - type Class, - type Doc, - type Permission, - type Ref, - type Role, - type RolesAssignment, - type Space, - type SpaceType, - type Tx, - type TxApplyIf, - type TxCUD, - type TxCreateDoc, - type TxMixin, - TxProcessor, - type TxRemoveDoc, - type TxUpdateDoc, - type TypedSpace, - type MeasureContext, - type SessionData, - type AccountUuid -} from '@hcengineering/core' -import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import { type Middleware, type TxMiddlewareResult, type PipelineContext } from '@hcengineering/server-core' - -import { BaseMiddleware } from '@hcengineering/server-core' - -/** - * @public - */ -export class SpacePermissionsMiddleware extends BaseMiddleware implements Middleware { - private whitelistSpaces = new Set>() - private assignmentBySpace: Record, RolesAssignment> = {} - private permissionsBySpace: Record, Record>> = {} - private typeBySpace: Record, Ref> = {} - wasInit: Promise | boolean = false - - static async create ( - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new SpacePermissionsMiddleware(context, next) - } - - private async init (ctx: MeasureContext): Promise { - if (this.wasInit === true) { - return - } - if (this.wasInit === false) { - this.wasInit = (async () => { - const spaces: Space[] = (await this.next?.findAll(ctx, core.class.Space, {})) ?? [] - - for (const space of spaces) { - if (this.isTypedSpace(space)) { - this.addRestrictedSpace(space) - } - } - - this.whitelistSpaces = new Set(spaces.filter((s) => !this.isTypedSpaceClass(s._class)).map((p) => p._id)) - })() - } - if (this.wasInit instanceof Promise) { - await this.wasInit - this.wasInit = true - } - } - - private getPermissions (): Permission[] { - return this.context.modelDb.findAllSync(core.class.Permission, {}) - } - - private getRoles (spaceTypeId: Ref): Role[] { - return this.context.modelDb.findAllSync(core.class.Role, { attachedTo: spaceTypeId }) - } - - private setPermissions ( - spaceId: Ref, - roles: Role[], - assignment: RolesAssignment, - permissions: Permission[] - ): void { - for (const role of roles) { - const roleMembers: AccountUuid[] = assignment[role._id] ?? [] - - for (const member of roleMembers) { - if (this.permissionsBySpace[spaceId][member] === undefined) { - this.permissionsBySpace[spaceId][member] = new Set() - } - - for (const permission of role.permissions) { - const p = permissions.find((p) => p._id === permission) - if (p === undefined) continue - this.permissionsBySpace[spaceId][member].add(p) - } - } - } - } - - private addRestrictedSpace (space: TypedSpace): void { - this.permissionsBySpace[space._id] = {} - - const spaceType = this.context.modelDb.findAllSync(core.class.SpaceType, { _id: space.type })[0] - - if (spaceType === undefined) { - return - } - - this.typeBySpace[space._id] = space.type - - const asMixin: RolesAssignment = this.context.hierarchy.as( - space, - spaceType.targetClass - ) as unknown as RolesAssignment - - const allPossibleRoles = this.context.modelDb.findAllSync(core.class.Role, {}) - const requiredValues: Record = {} - for (const role of allPossibleRoles) { - const v = asMixin[role._id] - if (v !== undefined) { - requiredValues[role._id] = asMixin[role._id] - } - } - - this.assignmentBySpace[space._id] = requiredValues - - this.setPermissions(space._id, this.getRoles(spaceType._id), asMixin, this.getPermissions()) - } - - private isTypedSpaceClass (_class: Ref>): boolean { - const h = this.context.hierarchy - - return h.isDerived(_class, core.class.TypedSpace) - } - - private isTypedSpace (space: Space): space is TypedSpace { - return this.isTypedSpaceClass(space._class) - } - - /** - * @private - * - * Checks if the required permission is present in the space for the given context - */ - private checkPermission (ctx: MeasureContext, space: Ref, tx: TxCUD): boolean { - const account = ctx.contextData.account - const permissions = this.permissionsBySpace[space]?.[account.uuid] ?? [] - for (const permission of permissions) { - if (permission.txClass === undefined || permission.txClass !== tx._class) continue - if (permission.objectClass !== undefined && permission.objectClass !== tx.objectClass) continue - return permission.forbid !== undefined ? !permission.forbid : true - } - - return true - } - - private throwForbidden (): void { - throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) - } - - // /** - // * @private - // * - // * Throws if the required permission is missing in the space for the given context - // */ - // private async needPermission (ctx: MeasureContext, space: Ref, id: Ref): Promise { - // if (this.checkPermission(ctx, space, id)) { - // return - // } - - // this.throwForbidden() - // } - - private handleCreate (tx: TxCUD): void { - const createTx = tx as TxCreateDoc - if (!this.context.hierarchy.isDerived(createTx.objectClass, core.class.Space)) return - if (this.isTypedSpaceClass(createTx.objectClass)) { - const res = TxProcessor.buildDoc2Doc([createTx]) - if (res != null) { - this.addRestrictedSpace(res) - } - } else { - this.whitelistSpaces.add(createTx.objectId) - } - } - - private handleMixin (tx: TxCUD): void { - if (!this.isTypedSpaceClass(tx.objectClass)) { - return - } - - const spaceId = tx.objectId - const spaceTypeId = this.typeBySpace[spaceId] - - if (spaceTypeId === undefined) { - return - } - - const spaceType = this.context.modelDb.findAllSync(core.class.SpaceType, { _id: spaceTypeId }).shift() - - if (spaceType === undefined) { - return - } - - const mixinDoc = tx as TxMixin - - if (mixinDoc.mixin !== spaceType.targetClass) { - return - } - - // Note: currently the whole assignment is always included into the mixin update - // so we can just rebuild the permissions - const assignment: RolesAssignment = mixinDoc.attributes as RolesAssignment - - const allPossibleRoles = this.context.modelDb.findAllSync(core.class.Role, {}) - const requiredValues: Record = {} - for (const role of allPossibleRoles) { - const v = assignment[role._id] - if (v !== undefined) { - requiredValues[role._id] = assignment[role._id] - } - } - - this.assignmentBySpace[spaceId] = requiredValues - - this.permissionsBySpace[tx.objectId] = {} - this.setPermissions(spaceId, this.getRoles(spaceType._id), assignment, this.getPermissions()) - } - - private handleRemove (tx: TxCUD): void { - const removeTx = tx as TxRemoveDoc - if (!this.context.hierarchy.isDerived(removeTx.objectClass, core.class.Space)) return - if (removeTx._class !== core.class.TxCreateDoc) return - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.permissionsBySpace[tx.objectId] - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.assignmentBySpace[tx.objectId] - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.typeBySpace[tx.objectId] - - this.whitelistSpaces.delete(tx.objectId) - } - - private isSpaceTxCUD (tx: TxCUD): tx is TxCUD { - return this.context.hierarchy.isDerived(tx.objectClass, core.class.Space) - } - - private isRoleTxCUD (tx: TxCUD): tx is TxCUD { - return this.context.hierarchy.isDerived(tx.objectClass, core.class.Role) - } - - private handlePermissionsUpdatesFromRoleTx (ctx: MeasureContext, actualTx: TxCUD): void { - if (actualTx._class !== core.class.TxUpdateDoc) { - return - } - - const targetSpaceTypeId = actualTx.attachedTo - if (targetSpaceTypeId === undefined) { - return - } - - if (!this.isRoleTxCUD(actualTx)) { - return - } - - // We are only interested in updates of the existing roles because: - // When role is created it always has empty set of permissions - // And it's not currently possible to delete a role - const updateTx = actualTx as TxUpdateDoc - - if (updateTx.operations.permissions === undefined) { - return - } - - // Find affected spaces - const affectedSpacesIds = Object.entries(this.typeBySpace) - .filter(([, typeId]) => typeId === targetSpaceTypeId) - .map(([spaceId]) => spaceId) as Ref[] - - for (const spaceId of affectedSpacesIds) { - const spaceTypeId = this.typeBySpace[spaceId] - - if (spaceTypeId === undefined) { - return - } - - const assignment: RolesAssignment = this.assignmentBySpace[spaceId] - const roles = this.getRoles(spaceTypeId) - const targetRole = roles.find((r) => r._id === updateTx.objectId) - - if (targetRole === undefined) { - continue - } - - targetRole.permissions = updateTx.operations.permissions - - this.permissionsBySpace[spaceId] = {} - this.setPermissions(spaceId, roles, assignment, this.getPermissions()) - } - } - - private handlePermissionsUpdatesFromTx (ctx: MeasureContext, tx: TxCUD): void { - if (this.isSpaceTxCUD(tx)) { - if (tx._class === core.class.TxCreateDoc) { - this.handleCreate(tx) - // } else if (tx._class === core.class.TxUpdateDoc) { - // Roles assignment in spaces are managed through the space type mixin - // so nothing to handle here - } else if (tx._class === core.class.TxMixin) { - this.handleMixin(tx) - } else if (tx._class === core.class.TxRemoveDoc) { - this.handleRemove(tx) - } - } - - this.handlePermissionsUpdatesFromRoleTx(ctx, tx) - } - - private processPermissionsUpdatesFromTx (ctx: MeasureContext, tx: Tx): void { - if (!TxProcessor.isExtendsCUD(tx._class)) { - return - } - - const cudTx = tx as TxCUD - this.handlePermissionsUpdatesFromTx(ctx, cudTx) - } - - async tx (ctx: MeasureContext, txes: Tx[]): Promise { - await this.init(ctx) - for (const tx of txes) { - this.processPermissionsUpdatesFromTx(ctx, tx) - this.checkPermissions(ctx, tx) - } - const res = await this.provideTx(ctx, txes) - for (const txd of ctx.contextData.broadcast.txes) { - this.processPermissionsUpdatesFromTx(ctx, txd) - } - return res - } - - protected checkPermissions (ctx: MeasureContext, tx: Tx): void { - if (tx._class === core.class.TxApplyIf) { - const applyTx = tx as TxApplyIf - - for (const t of applyTx.txes) { - this.checkPermissions(ctx, t) - } - return - } - - const cudTx = tx as TxCUD - const h = this.context.hierarchy - const isSpace = h.isDerived(cudTx.objectClass, core.class.Space) - - this.checkSpacePermissions(ctx, cudTx, cudTx.objectSpace) - if (isSpace) { - this.checkSpacePermissions(ctx, cudTx, cudTx.objectId as Ref) - } - } - - private checkSpacePermissions (ctx: MeasureContext, cudTx: TxCUD, targetSpaceId: Ref): void { - if (this.whitelistSpaces.has(targetSpaceId)) { - return - } - // NOTE: move this checking logic later to be defined in some server plugins? - // so they can contribute checks into the middleware for their custom permissions? - if (!this.checkPermission(ctx, targetSpaceId as Ref, cudTx)) { - this.throwForbidden() - } - } -} diff --git a/server/middleware/src/spaceSecurity.ts b/server/middleware/src/spaceSecurity.ts deleted file mode 100644 index d0022d43aa1..00000000000 --- a/server/middleware/src/spaceSecurity.ts +++ /dev/null @@ -1,742 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import core, { - type Account, - AccountRole, - type AccountUuid, - type AttachedDoc, - type Class, - clone, - type Collaborator, - type Doc, - type DocumentQuery, - type Domain, - DOMAIN_MODEL, - type FindResult, - generateId, - getClassCollaborators, - type LookupData, - type MeasureContext, - type ObjQueryType, - type Position, - type PullArray, - type Ref, - type SearchOptions, - type SearchQuery, - type SearchResult, - type SessionData, - shouldShowArchived, - type Space, - systemAccountUuid, - toFindResult, - type Tx, - type TxCreateDoc, - type TxCUD, - TxProcessor, - type TxRemoveDoc, - type TxUpdateDoc, - type TxWorkspaceEvent, - WorkspaceEvent -} from '@hcengineering/core' -import { - BaseMiddleware, - type Middleware, - type PipelineContext, - type ServerFindOptions, - type TxMiddlewareResult -} from '@hcengineering/server-core' -import { isOwner, isSystem } from './utils' - -type SpaceWithMembers = Pick - -/** - * @public - */ -export class SpaceSecurityMiddleware extends BaseMiddleware implements Middleware { - private allowedSpaces: Record[]> = {} - private readonly spacesMap = new Map, SpaceWithMembers>() - private readonly privateSpaces = new Set>() - private readonly _domainSpaces = new Map> | Promise>>>() - private readonly publicSpaces = new Set>() - private readonly systemSpaces = new Set>() - - wasInit: Promise | boolean = false - - private readonly mainSpaces = new Set([ - core.space.Configuration, - core.space.DerivedTx, - core.space.Model, - core.space.Space, - core.space.Workspace, - core.space.Tx - ]) - - private constructor ( - private readonly skipFindCheck: boolean, - context: PipelineContext, - next?: Middleware - ) { - super(context, next) - } - - static async create ( - skipFindCheck: boolean, - ctx: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new SpaceSecurityMiddleware(skipFindCheck, context, next) - } - - private resyncDomains (): void { - this.wasInit = false - } - - private addMemberSpace (member: AccountUuid, space: Ref): void { - const arr = this.allowedSpaces[member] ?? [] - arr.push(space) - this.allowedSpaces[member] = arr - } - - private addSpace (space: SpaceWithMembers): void { - this.spacesMap.set(space._id, space) - if (space.private) { - this.privateSpaces.add(space._id) - } else { - this.publicSpaces.add(space._id) - } - for (const member of space.members) { - this.addMemberSpace(member, space._id) - } - } - - async init (ctx: MeasureContext): Promise { - if (this.wasInit === true) { - return - } - if (this.wasInit === false) { - this.wasInit = (async () => { - await ctx.with('init-space-security', {}, async (ctx) => { - ctx.contextData = undefined - const spaces: SpaceWithMembers[] = - (await this.next?.findAll( - ctx, - core.class.Space, - {}, - { - projection: { - archived: 1, - private: 1, - _class: 1, - _id: 1, - members: 1 - } - } - )) ?? [] - this.spacesMap.clear() - this.publicSpaces.clear() - this.systemSpaces.clear() - for (const space of spaces) { - if (space._class === core.class.SystemSpace) { - this.systemSpaces.add(space._id) - } else { - this.addSpace(space) - } - } - }) - })() - } - if (this.wasInit instanceof Promise) { - await this.wasInit - this.wasInit = true - } - } - - private removeMemberSpace (member: AccountUuid, space: Ref): void { - const arr = this.allowedSpaces[member] - if (arr !== undefined) { - const index = arr.findIndex((p) => p === space) - if (index !== -1) { - arr.splice(index, 1) - this.allowedSpaces[member] = arr - } - } - } - - private removeSpace (_id: Ref): void { - const space = this.spacesMap.get(_id) - if (space !== undefined) { - for (const member of space.members) { - this.removeMemberSpace(member, space._id) - } - } - this.spacesMap.delete(_id) - this.privateSpaces.delete(_id) - this.publicSpaces.delete(_id) - } - - private async handeCollaborator (ctx: MeasureContext, tx: TxCUD): Promise { - if (!this.context.hierarchy.isDerived(tx.objectClass, core.class.Collaborator)) return - if (tx._class === core.class.TxCreateDoc) { - const collab = TxProcessor.createDoc2Doc(tx as TxCreateDoc) - this.handleChangeCollaborator(ctx, collab) - } else if (tx._class === core.class.TxRemoveDoc) { - const collab = (await this.next?.findAll(ctx, core.class.Collaborator, { - _id: tx.objectId - })) as Collaborator[] - if (collab.length === 0) return - this.handleChangeCollaborator(ctx, collab[0]) - } - } - - private handleChangeCollaborator (ctx: MeasureContext, collab: Collaborator): void { - const collabSec = this.context.modelDb.findAllSync(core.class.ClassCollaborators, { - attachedTo: collab.attachedToClass - })[0] - if (collabSec?.provideSecurity === true) { - for (const val of ctx.contextData.socialStringsToUsers.values()) { - if ( - val.accontUuid === collab.collaborator && - [AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(val.role) - ) { - this.brodcastEvent(ctx, [val.accontUuid]) - } - } - } - } - - private handleCreate (tx: TxCUD): void { - const createTx = tx as TxCreateDoc - if (!this.context.hierarchy.isDerived(createTx.objectClass, core.class.Space)) return - if (createTx.objectClass === core.class.SystemSpace) { - this.systemSpaces.add(createTx.objectId) - } else { - const res = TxProcessor.createDoc2Doc(createTx) - this.addSpace(res) - } - } - - private pushMembersHandle ( - ctx: MeasureContext, - addedMembers: AccountUuid | Position, - space: Ref - ): void { - if (typeof addedMembers === 'object') { - for (const member of addedMembers.$each) { - this.addMemberSpace(member, space) - } - this.brodcastEvent(ctx, addedMembers.$each, space) - } else { - this.addMemberSpace(addedMembers, space) - this.brodcastEvent(ctx, [addedMembers], space) - } - } - - private pullMembersHandle ( - ctx: MeasureContext, - removedMembers: Partial | PullArray, - space: Ref - ): void { - if (typeof removedMembers === 'object') { - const { $in } = removedMembers as PullArray - if ($in !== undefined) { - for (const member of $in) { - this.removeMemberSpace(member, space) - } - this.brodcastEvent(ctx, $in, space) - } - } else { - this.removeMemberSpace(removedMembers, space) - this.brodcastEvent(ctx, [removedMembers], space) - } - } - - private syncMembers (ctx: MeasureContext, members: AccountUuid[], space: SpaceWithMembers): void { - const oldMembers = new Set(space.members) - const newMembers = new Set(members) - const changed: AccountUuid[] = [] - for (const old of oldMembers) { - if (!newMembers.has(old)) { - this.removeMemberSpace(old, space._id) - changed.push(old) - } - } - for (const newMem of newMembers) { - if (!oldMembers.has(newMem)) { - this.addMemberSpace(newMem, space._id) - changed.push(newMem) - } - } - // TODO: consider checking if updated social strings actually change assigned accounts - if (changed.length > 0) { - this.brodcastEvent(ctx, changed, space._id) - } - } - - private brodcastEvent (ctx: MeasureContext, users: AccountUuid[], space?: Ref): void { - const targets = this.getTargets(users) - const tx: TxWorkspaceEvent = { - _class: core.class.TxWorkspaceEvent, - _id: generateId(), - event: WorkspaceEvent.SecurityChange, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - objectSpace: space ?? core.space.DerivedTx, - space: core.space.DerivedTx, - params: null - } - ctx.contextData.broadcast.txes.push(tx) - ctx.contextData.broadcast.targets['security' + tx._id] = async (it) => { - // TODO: I'm not sure it is called - if (it._id === tx._id) { - return { - target: targets - } - } - } - } - - private broadcastNonMembers (ctx: MeasureContext, space: SpaceWithMembers): void { - const members = space?.members ?? [] - - this.brodcastEvent(ctx, members, space._id) - } - - private broadcastAll (ctx: MeasureContext, space: SpaceWithMembers): void { - const { socialStringsToUsers } = ctx.contextData - const accounts = Array.from(new Set(Array.from(socialStringsToUsers.values()).map((v) => v.accontUuid))) - - this.brodcastEvent(ctx, accounts, space._id) - } - - private async handleUpdate (ctx: MeasureContext, tx: TxCUD): Promise { - await this.init(ctx) - - const updateDoc = tx as TxUpdateDoc - if (!this.context.hierarchy.isDerived(updateDoc.objectClass, core.class.Space)) return - - const space = this.spacesMap.get(updateDoc.objectId) - if (space !== undefined) { - if (updateDoc.operations.private !== undefined) { - if (updateDoc.operations.private) { - this.privateSpaces.add(updateDoc.objectId) - this.publicSpaces.delete(updateDoc.objectId) - this.broadcastNonMembers(ctx, space) - } else if (!updateDoc.operations.private) { - this.privateSpaces.delete(updateDoc.objectId) - this.publicSpaces.add(updateDoc.objectId) - this.broadcastNonMembers(ctx, space) - } - } - - if (updateDoc.operations.members !== undefined) { - this.syncMembers(ctx, updateDoc.operations.members, space) - } - if (updateDoc.operations.$push?.members !== undefined) { - this.pushMembersHandle(ctx, updateDoc.operations.$push.members, space._id) - } - - if (updateDoc.operations.$pull?.members !== undefined) { - this.pullMembersHandle(ctx, updateDoc.operations.$pull.members, space._id) - } - if (updateDoc.operations.archived !== undefined) { - this.broadcastAll(ctx, space) - } - const updatedSpace = TxProcessor.updateDoc2Doc(space as any, updateDoc) - this.spacesMap.set(updateDoc.objectId, updatedSpace) - } - } - - private handleRemove (tx: TxCUD): void { - const removeTx = tx as TxRemoveDoc - if (!this.context.hierarchy.isDerived(removeTx.objectClass, core.class.Space)) return - if (removeTx._class !== core.class.TxRemoveDoc) return - this.removeSpace(tx.objectId) - } - - private async handleTx (ctx: MeasureContext, tx: TxCUD): Promise { - await this.init(ctx) - if (tx._class === core.class.TxCreateDoc) { - this.handleCreate(tx) - } else if (tx._class === core.class.TxUpdateDoc) { - await this.handleUpdate(ctx, tx) - } else if (tx._class === core.class.TxRemoveDoc) { - this.handleRemove(tx) - } - } - - getTargets (accounts: AccountUuid[]): AccountUuid[] { - const res = Array.from(new Set(accounts)) - // We need to add system account for targets for integrations to work properly - res.push(systemAccountUuid) - - return res - } - - private async processTxSpaceDomain (sctx: MeasureContext, actualTx: TxCUD): Promise { - if (actualTx._class === core.class.TxCreateDoc) { - const ctx = actualTx as TxCreateDoc - const doc = TxProcessor.createDoc2Doc(ctx) - const domain = this.context.hierarchy.getDomain(ctx.objectClass) - const key = this.getKey(domain) - const space = (doc as any)[key] - if (space === undefined) return - ;(await this.getDomainSpaces(sctx, domain)).add(space) - } else if (actualTx._class === core.class.TxUpdateDoc) { - const updTx = actualTx as TxUpdateDoc - const domain = this.context.hierarchy.getDomain(updTx.objectClass) - const key = this.getKey(domain) - const space = (updTx.operations as any)[key] - if (space !== undefined) { - ;(await this.getDomainSpaces(sctx, domain)).add(space) - } - } - } - - private async processTx (ctx: MeasureContext, tx: Tx): Promise { - const h = this.context.hierarchy - if (TxProcessor.isExtendsCUD(tx._class)) { - const cudTx = tx as TxCUD - const isSpace = h.isDerived(cudTx.objectClass, core.class.Space) - if (isSpace) { - await this.handleTx(ctx, cudTx as TxCUD) - } else { - await this.handeCollaborator(ctx, cudTx as TxCUD) - } - await this.processTxSpaceDomain(ctx, tx as TxCUD) - } else if (tx._class === core.class.TxWorkspaceEvent) { - const event = tx as TxWorkspaceEvent - if (event.event === WorkspaceEvent.BulkUpdate) { - this.resyncDomains() - } - } - } - - async tx (ctx: MeasureContext, txes: Tx[]): Promise { - await this.init(ctx) - const processed = new Set>() - ctx.contextData.contextCache.set('processed', processed) - for (const tx of txes) { - processed.add(tx._id) - await this.processTx(ctx, tx) - } - return await this.provideTx(ctx, txes) - } - - override async handleBroadcast (ctx: MeasureContext): Promise { - const processed: Set> = ctx.contextData.contextCache.get('processed') ?? new Set>() - ctx.contextData.contextCache.set('processed', processed) - for (const txd of ctx.contextData.broadcast.txes) { - if (!processed.has(txd._id)) { - await this.processTx(ctx, txd) - } - } - for (const tx of ctx.contextData.broadcast.txes) { - if (TxProcessor.isExtendsCUD(tx._class)) { - // TODO: Do we need security check here? - const cudTx = tx as TxCUD - await this.processTxSpaceDomain(ctx, cudTx) - } else if (tx._class === core.class.TxWorkspaceEvent) { - const event = tx as TxWorkspaceEvent - if (event.event === WorkspaceEvent.BulkUpdate) { - this.resyncDomains() - } - } - } - - ctx.contextData.broadcast.targets.spaceSec = async (tx) => { - const cud = tx as TxCUD - if (cud.objectClass === undefined) return undefined - - // For system and main spaces broadcast to all users except guests that are not collaborators for objects with collab security enabled - if (this.systemSpaces.has(tx.objectSpace) || this.mainSpaces.has(tx.objectSpace)) { - const collabSec = getClassCollaborators(this.context.modelDb, this.context.hierarchy, cud.objectClass) - if (collabSec?.provideSecurity === true) { - const guests = new Set() - for (const val of ctx.contextData.socialStringsToUsers.values()) { - if ([AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(val.role)) { - guests.add(val.accontUuid) - } - } - const collabs = (await this.next?.findAll(ctx, core.class.Collaborator, { - attachedTo: cud.objectId - })) as Collaborator[] - for (const collab of collabs) { - guests.delete(collab.collaborator) - } - return { exclude: Array.from(guests) } - } - return undefined - } - - const space = this.spacesMap.get(tx.objectSpace) - if (space === undefined) return undefined - - // For all other spaces broadcast to space members + guests that are collaborators for objects with collab security enabled - let collabTargets: AccountUuid[] = [] - const collabSec = getClassCollaborators(this.context.modelDb, this.context.hierarchy, cud.objectClass) - if (collabSec?.provideSecurity === true) { - const guests = new Set() - for (const val of ctx.contextData.socialStringsToUsers.values()) { - if ([AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(val.role)) { - guests.add(val.accontUuid) - } - } - const collaboratorObjs = (await this.next?.findAll(ctx, core.class.Collaborator, { - attachedTo: cud.objectId - })) as Collaborator[] - - collabTargets = collaboratorObjs.map((it) => it.collaborator).filter((it) => guests.has(it)) - } - const spaceTargets = space.members.length === 0 ? [] : this.getTargets(space?.members) - const target = [...collabTargets, ...spaceTargets] - - return target.length === 0 ? undefined : { target } - } - - await this.next?.handleBroadcast(ctx) - } - - private getAllAllowedSpaces ( - account: Account, - isData: boolean, - showArchived: boolean, - forSearch: boolean = false - ): Ref[] { - const userSpaces = this.allowedSpaces[account.uuid] ?? [] - let res = [...Array.from(userSpaces), account.uuid as unknown as Ref, ...this.mainSpaces] - if (!forSearch || ![AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(account.role)) { - res = [...res, ...this.systemSpaces] - } - const ignorePublicSpaces = isData || account.role === AccountRole.ReadOnlyGuest - const unfilteredRes = ignorePublicSpaces ? res : [...res, ...this.publicSpaces] - if (showArchived) { - return unfilteredRes - } - return unfilteredRes.filter((p) => this.spacesMap.get(p)?.archived !== true) - } - - async getDomainSpaces (ctx: MeasureContext, domain: Domain): Promise>> { - let domainSpaces = this._domainSpaces.get(domain) - if (domainSpaces === undefined) { - const p = ( - this.next?.groupBy, Doc>(ctx, domain, this.getKey(domain)) ?? Promise.resolve(new Map()) - ).then((r) => new Set>(r.keys())) - this._domainSpaces.set(domain, p) - domainSpaces = await p - this._domainSpaces.set(domain, domainSpaces) - } - return domainSpaces instanceof Promise ? await domainSpaces : domainSpaces - } - - private async filterByDomain ( - ctx: MeasureContext, - domain: Domain, - spaces: Ref[] - ): Promise<{ result: Set>, allDomainSpaces: boolean, domainSpaces: Set> }> { - const domainSpaces = await this.getDomainSpaces(ctx, domain) - const result = new Set(spaces.filter((p) => domainSpaces.has(p))) - return { - result, - allDomainSpaces: result.size === domainSpaces.size, - domainSpaces - } - } - - private async mergeQuery( - ctx: MeasureContext, - account: Account, - query: ObjQueryType, - domain: Domain, - isSpace: boolean, - showArchived: boolean - ): Promise | undefined> { - const spaces = await this.filterByDomain(ctx, domain, this.getAllAllowedSpaces(account, !isSpace, showArchived)) - if (query == null) { - if (spaces.allDomainSpaces) { - return undefined - } - return { $in: Array.from(spaces.result) } - } - if (typeof query === 'string') { - if (!spaces.result.has(query)) { - return { $in: [] } - } - } else if (query.$in != null) { - query.$in = query.$in.filter((p) => spaces.result.has(p)) - if (query.$in.length === spaces.domainSpaces.size) { - // all domain spaces - delete query.$in - } - } else { - if (spaces.allDomainSpaces) { - delete query.$in - } else { - query.$in = Array.from(spaces.result) - } - } - if (Object.keys(query).length === 0) { - return undefined - } - return query - } - - private getKey (domain: string): string { - return domain === 'tx' ? 'objectSpace' : domain === 'space' ? '_id' : 'space' - } - - override async findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: ServerFindOptions - ): Promise> { - await this.init(ctx) - - const domain = this.context.hierarchy.getDomain(_class) - const newQuery = clone(query) - const account = ctx.contextData.account - const isSpace = this.context.hierarchy.isDerived(_class, core.class.Space) - const field = this.getKey(domain) - const showArchived: boolean = shouldShowArchived(newQuery, options) - - let clientFilterSpaces: Set> | undefined - - if (!isSystem(account, ctx) && account.role !== AccountRole.DocGuest && domain !== DOMAIN_MODEL) { - if (!isOwner(account, ctx) || !isSpace || !showArchived) { - if (newQuery[field] !== undefined) { - const res = await this.mergeQuery(ctx, account, newQuery[field], domain, isSpace, showArchived) - if (res === undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete newQuery[field] - } else { - newQuery[field] = res - if (typeof res === 'object') { - if (Array.isArray(res.$in) && res.$in.length === 1 && Object.keys(res).length === 1) { - newQuery[field] = res.$in[0] - } - } - } - } else { - const spaces = await this.filterByDomain( - ctx, - domain, - this.getAllAllowedSpaces(account, !isSpace, showArchived) - ) - if (spaces.allDomainSpaces) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete newQuery[field] - } else if (spaces.result.size === 1) { - newQuery[field] = Array.from(spaces.result)[0] - if (options !== undefined) { - options.allowedSpaces = Array.from(spaces.result) - } else { - options = { allowedSpaces: Array.from(spaces.result) } - } - } else { - // Check if spaces are greater than 85% of all domain spaces. In this case, return all and filter on the client. - if (spaces.result.size / spaces.domainSpaces.size > 0.85 && options?.limit === undefined) { - clientFilterSpaces = spaces.result - delete newQuery.space - } else { - newQuery[field] = { $in: Array.from(spaces.result) } - if (options !== undefined) { - options.allowedSpaces = Array.from(spaces.result) - } else { - options = { allowedSpaces: Array.from(spaces.result) } - } - } - } - } - } - } - - let findResult = await this.provideFindAll(ctx, _class, !this.skipFindCheck ? newQuery : query, options) - if (clientFilterSpaces !== undefined) { - const cfs = clientFilterSpaces - findResult = toFindResult( - findResult.filter((it) => cfs.has((it as any)[field])), - findResult.total, - findResult.lookupMap - ) - } - if (!isOwner(account, ctx) && account.role !== AccountRole.DocGuest) { - if (options?.lookup !== undefined) { - for (const object of findResult) { - if (object.$lookup !== undefined) { - this.filterLookup(ctx, object.$lookup, showArchived) - } - } - } - } - return findResult - } - - override async searchFulltext ( - ctx: MeasureContext, - query: SearchQuery, - options: SearchOptions - ): Promise { - await this.init(ctx) - const newQuery = { ...query } - const account = ctx.contextData.account - if (!isSystem(account, ctx)) { - const allSpaces = this.getAllAllowedSpaces(account, true, false, true) - if (query.classes !== undefined) { - const res = new Set>() - const passedDomains = new Set() - for (const _class of query.classes) { - const domain = this.context.hierarchy.getDomain(_class) - if (passedDomains.has(domain)) { - continue - } - passedDomains.add(domain) - const spaces = await this.filterByDomain(ctx, domain, allSpaces) - for (const space of spaces.result) { - res.add(space) - } - } - newQuery.spaces = [...res] - } else { - newQuery.spaces = allSpaces - } - } - const result = await this.provideSearchFulltext(ctx, newQuery, options) - return result - } - - filterLookup(ctx: MeasureContext, lookup: LookupData, showArchived: boolean): void { - if (Object.keys(lookup).length === 0) return - const account = ctx.contextData.account - if (isSystem(account, ctx)) return - const allowedSpaces = new Set(this.getAllAllowedSpaces(account, true, showArchived)) - for (const key in lookup) { - const val = lookup[key] - if (Array.isArray(val)) { - const arr: AttachedDoc[] = [] - for (const value of val) { - if (allowedSpaces.has(value.space)) { - arr.push(value) - } - } - lookup[key] = arr as any - } else if (val !== undefined) { - if (!allowedSpaces.has(val.space)) { - lookup[key] = undefined - } - } - } - } -} diff --git a/server/middleware/src/tests/queryJoiner.spec.ts b/server/middleware/src/tests/queryJoiner.spec.ts deleted file mode 100644 index b289cd88509..00000000000 --- a/server/middleware/src/tests/queryJoiner.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import core, { MeasureMetricsContext, toFindResult } from '@hcengineering/core' -import type { SessionFindAll } from '@hcengineering/server-core' -import { QueryJoiner } from '../queryJoin' - -describe('test query joiner', () => { - it('test find', async () => { - let count = 0 - const findT: SessionFindAll = async (ctx, _class, query, options) => { - await new Promise((resolve) => { - count++ - setTimeout(resolve, 100) - }) - return toFindResult([]) - } - const joiner = new QueryJoiner() - const ctx = new MeasureMetricsContext('test', {}) - - const p1 = joiner.query(ctx, core.class.Class, (ctx) => findT(ctx, core.class.Class, {})) - const p2 = joiner.query(ctx, core.class.Class, (ctx) => findT(ctx, core.class.Class, {})) - await Promise.all([p1, p2]) - expect(count).toBe(1) - expect((joiner as any).queries.size).toBe(0) - }) -}) diff --git a/server/middleware/src/triggers.ts b/server/middleware/src/triggers.ts deleted file mode 100644 index 51fb2b95f2d..00000000000 --- a/server/middleware/src/triggers.ts +++ /dev/null @@ -1,542 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type AttachedDoc, - type Class, - ClassifierKind, - type Collection, - DOMAIN_MODEL, - type Doc, - type DocumentUpdate, - type LowLevelStorage, - type MeasureContext, - type Mixin, - type PersonId, - type Ref, - type SessionData, - type Tx, - type TxCUD, - TxFactory, - type TxRemoveDoc, - type TxUpdateDoc, - addOperation, - toFindResult, - withContext -} from '@hcengineering/core' -import { PlatformError, getResource, unknownError } from '@hcengineering/platform' -import serverCore, { - type Middleware, - type ObjectDDParticipant, - type PipelineContext, - type ServerFindOptions, - type ServiceAdaptersManager, - type StorageAdapter, - type TriggerControl, - type TxMiddlewareResult, - BaseMiddleware, - SessionDataImpl, - type SessionFindAll, - Triggers -} from '@hcengineering/server-core' -import { filterBroadcastOnly } from './utils' - -/** - * @public - */ -export class TriggersMiddleware extends BaseMiddleware implements Middleware { - triggers: Triggers - storageAdapter!: StorageAdapter - cache = new Map() - intervalId: NodeJS.Timeout - - constructor (context: PipelineContext, next: Middleware | undefined) { - super(context, next) - this.triggers = new Triggers(this.context.hierarchy) - this.intervalId = setInterval( - () => { - this.cache.clear() - }, - 30 * 60 * 1000 - ) - } - - async close (): Promise { - clearInterval(this.intervalId) - } - - static async create (ctx: MeasureContext, context: PipelineContext, next?: Middleware): Promise { - // we need to init triggers from model first. - const triggers = new TriggersMiddleware(context, next) - await triggers.init(ctx) - return triggers - } - - async init (ctx: MeasureContext): Promise { - if (this.context.storageAdapter == null) { - throw new PlatformError(unknownError('Storage adapter should be specified')) - } - if (this.context.lowLevelStorage == null) { - throw new PlatformError(unknownError('Low level storage should be specified')) - } - this.storageAdapter = this.context.storageAdapter - this.triggers.init(this.context.modelDb) - } - - async tx (ctx: MeasureContext, tx: Tx[]): Promise { - await this.triggers.tx(tx) - const result = await this.provideTx(ctx, tx) - - const ftx = filterBroadcastOnly(tx, this.context.hierarchy) - if (ftx.length > 0) { - await this.processDerived(ctx, ftx) - } - return result - } - - private async processDerived (ctx: MeasureContext, txes: Tx[]): Promise { - const findAll: SessionFindAll = async (ctx, _class, query, options) => { - const _ctx: MeasureContext = (options as ServerFindOptions)?.ctx ?? ctx - delete (options as ServerFindOptions)?.ctx - if (_ctx.contextData !== undefined) { - _ctx.contextData.isTriggerCtx = true - } - - // Use live query - const results = await this.findAll(_ctx, _class, query, options) - return toFindResult( - results.map((v) => { - return this.context.hierarchy.updateLookupMixin(_class, v, options) - }), - results.total - ) - } - - const removed = await ctx.with('process-remove', {}, (ctx) => this.processRemove(ctx, txes, findAll)) - const collections = await ctx.with('process-collection', {}, (ctx) => this.processCollection(ctx, txes, findAll)) - const moves = await ctx.with('process-move', {}, (ctx) => this.processMove(ctx, txes, findAll)) - - const triggerControl: Omit = { - removedMap: ctx.contextData.removedMap, - workspace: this.context.workspace, - lowLevel: this.context.lowLevelStorage as LowLevelStorage, - branding: this.context.branding, - storageAdapter: this.storageAdapter, - serviceAdaptersManager: this.context.serviceAdapterManager as ServiceAdaptersManager, - findAll, - queue: this.context.queue, - contextCache: ctx.contextData.contextCache, - modelDb: this.context.modelDb, - hierarchy: this.context.hierarchy, - cache: this.cache, - userStatusMap: this.context.userStatusMap ?? new Map(), - domainRequest: async (ctx, domain, params) => { - return (await this.context.head?.domainRequest(ctx, domain, params)) ?? { domain, value: undefined } - }, - apply: async (ctx, tx, needResult) => { - if (needResult === true) { - return (await this.context.derived?.tx(ctx, tx)) ?? {} - } - return {} - }, - // Will create a live query if missing and return values immediately if already asked. - queryFind: (ctx: MeasureContext, _class, query, options) => { - const domain = this.context.hierarchy.findDomain(_class) - return ctx.with('query-find', { domain }, (ctx) => { - const { ctx: octx, ...pureOptions } = (options as ServerFindOptions) ?? {} - return addOperation( - ctx, - 'query-find', - { domain, _class, query: query as any, options: pureOptions as any }, - () => - // We sure ctx is required to be passed - this.context.liveQuery?.queryFind(_class, query) ?? - this.provideFindAll(ctx, _class, query, { ...options }) - ) - }) - } - } - - const triggers = await this.processSyncTriggers(ctx, txes, triggerControl, findAll) - - const derived = [...removed, ...collections, ...moves, ...triggers] - - if (derived.length > 0) { - await this.processDerivedTxes(ctx, derived) - } - - const performSync = (ctx as MeasureContext).contextData.isAsyncContext ?? false - - if (performSync) { - await this.processAsyncTriggers(ctx, triggerControl, findAll, txes, triggers) - } else { - ctx.contextData.asyncRequests = [ - ...(ctx.contextData.asyncRequests ?? []), - async (_ctx, id?: string) => { - // Just replace id of previous context - ctx.id = id - // In case of async context, we execute both async and sync triggers as sync - await this.processAsyncTriggers(ctx, triggerControl, findAll, txes, triggers) - } - ] - } - } - - @withContext('process-sync-triggers') - processSyncTriggers ( - ctx: MeasureContext, - txes: Tx[], - triggerControl: Omit, - findAll: SessionFindAll - ): Promise { - return this.triggers.apply( - ctx, - txes, - { - ...triggerControl, - ctx, - findAll, - txes: [...txes] - }, - 'sync' - ) - } - - @withContext('process-async-triggers') - async processAsyncTriggers ( - ctx: MeasureContext, - triggerControl: Omit, - findAll: Middleware['findAll'], - txes: Tx[], // original txes - syncResult: Tx[] // sync result txes - ): Promise { - const sctx = ctx.contextData - const asyncContextData: SessionDataImpl = new SessionDataImpl( - sctx.account, - sctx.sessionId, - sctx.admin, - { txes: [], targets: {}, queue: [], sessions: {} }, - this.context.workspace, - true, - sctx.removedMap, - sctx.contextCache, - this.context.modelDb, - sctx.socialStringsToUsers, - sctx.service, - sctx.grant - ) - ctx.contextData = asyncContextData - const aresult = await this.triggers.apply( - ctx, - txes, - { - ...triggerControl, - ctx, - findAll, - txes: [...txes, ...syncResult] - }, - 'async' - ) - - if (aresult.length > 0) { - await ctx.with('process-async-result', {}, async (ctx) => { - await this.processDerivedTxes(ctx, aresult) - // We need to send all to recipients - await this.context.head?.handleBroadcast(ctx) - }) - } else if (ctx.contextData.hasDomainBroadcast === true) { - await ctx.with('broadcast-async-domain-request', {}, async (ctx) => { - await this.context.head?.handleBroadcast(ctx) - }) - } - } - - private async processDerivedTxes (ctx: MeasureContext, derived: Tx[]): Promise { - if (derived.length > 0) { - derived.sort((a, b) => a.modifiedOn - b.modifiedOn) - await this.context.derived?.tx(ctx, derived) - // We need to perform broadcast here - } - } - - private getCollectionUpdateTx( - _id: Ref, - _class: Ref>, - modifiedBy: PersonId, - modifiedOn: number, - attachedTo: Pick, - update: DocumentUpdate - ): Tx { - const txFactory = new TxFactory(modifiedBy, true) - const baseClass = this.context.hierarchy.getBaseClass(_class) - if (baseClass !== _class) { - // Mixin operation is required. - const tx = txFactory.createTxMixin(_id, attachedTo._class, attachedTo.space, _class, update) - tx.modifiedOn = modifiedOn - - return tx - } else { - const tx = txFactory.createTxUpdateDoc(_class, attachedTo.space, _id, update) - tx.modifiedOn = modifiedOn - - return tx - } - } - - private async processRemove (ctx: MeasureContext, txes: Tx[], findAll: SessionFindAll): Promise { - const result: Tx[] = [] - - for (const tx of txes) { - if (!this.context.hierarchy.isDerived(tx._class, core.class.TxRemoveDoc)) { - continue - } - const rtx = tx as TxRemoveDoc - const object = ctx.contextData.removedMap.get(rtx.objectId) - if (object === undefined) { - continue - } - result.push(...(await this.deleteClassCollections(ctx, object._class, rtx.objectId, findAll))) - const _class = this.context.hierarchy.findClass(object._class) - if (_class !== undefined) { - const mixins = this.getMixins(object._class, object) - for (const mixin of mixins) { - result.push(...(await this.deleteClassCollections(ctx, mixin, rtx.objectId, findAll, object._class))) - } - - result.push(...(await this.deleteRelatedDocuments(ctx, object, findAll))) - } - } - return result - } - - private async deleteClassCollections ( - ctx: MeasureContext, - _class: Ref>, - objectId: Ref, - findAll: SessionFindAll, - to?: Ref> - ): Promise { - const attributes = this.context.hierarchy.getAllAttributes(_class, to) - const result: Tx[] = [] - for (const attribute of attributes) { - if (this.context.hierarchy.isDerived(attribute[1].type._class, core.class.Collection)) { - const collection = attribute[1].type as Collection - const allAttached = await findAll(ctx, collection.of, { attachedTo: objectId }) - for (const attached of allAttached) { - result.push(...this.deleteObject(ctx, attached, ctx.contextData.removedMap)) - } - } - } - return result - } - - private async updateCollection ( - ctx: MeasureContext, - colTx: TxUpdateDoc, - findAll: SessionFindAll - ): Promise { - if (colTx.attachedTo === undefined || colTx.attachedToClass === undefined || colTx.collection === undefined) { - return [] - } - - const _id = colTx.attachedTo - const _class = colTx.attachedToClass - const { operations } = colTx - - if ( - this.context.hierarchy.getDomain(_class) === DOMAIN_MODEL // We could not update increments for model classes - ) { - return [] - } - - if (operations?.attachedTo === undefined || operations.attachedTo === _id) { - return [] - } - - const oldAttachedTo = (await findAll(ctx, _class, { _id }, { limit: 1 }))[0] - let oldTx: Tx | null = null - if (oldAttachedTo !== undefined) { - const attr = this.context.hierarchy.findAttribute(oldAttachedTo._class, colTx.collection) - - if (attr !== undefined) { - oldTx = this.getCollectionUpdateTx(_id, _class, colTx.modifiedBy, colTx.modifiedOn, oldAttachedTo, { - $inc: { [colTx.collection]: -1 } - }) - } - } - - const newAttachedToClass = operations.attachedToClass ?? _class - const newAttachedToCollection = operations.collection ?? colTx.collection - const newAttachedTo = (await findAll(ctx, newAttachedToClass, { _id: operations.attachedTo }, { limit: 1 }))[0] - let newTx: Tx | null = null - const newAttr = this.context.hierarchy.findAttribute(newAttachedToClass, newAttachedToCollection) - if (newAttachedTo !== undefined && newAttr !== undefined) { - newTx = this.getCollectionUpdateTx( - newAttachedTo._id, - newAttachedTo._class, - colTx.modifiedBy, - colTx.modifiedOn, - newAttachedTo, - { $inc: { [newAttachedToCollection]: 1 } } - ) - } - - return [...(oldTx !== null ? [oldTx] : []), ...(newTx !== null ? [newTx] : [])] - } - - private async processCollection ( - ctx: MeasureContext, - txes: Tx[], - findAll: SessionFindAll - ): Promise { - const result: Tx[] = [] - for (const tx of txes) { - const colTx = tx as TxCUD - if (colTx.attachedTo !== undefined && colTx.attachedToClass !== undefined && colTx.collection !== undefined) { - const _id = colTx.attachedTo - const _class = colTx.attachedToClass - - // Skip model operations - if (this.context.hierarchy.getDomain(_class) === DOMAIN_MODEL) { - // We could not update increments for model classes - continue - } - - const isCreateTx = colTx._class === core.class.TxCreateDoc - const isDeleteTx = colTx._class === core.class.TxRemoveDoc - const isUpdateTx = colTx._class === core.class.TxUpdateDoc - if (isUpdateTx) { - result.push(...(await this.updateCollection(ctx, colTx as TxUpdateDoc, findAll))) - } - - if ((isCreateTx || isDeleteTx) && !ctx.contextData.removedMap.has(_id)) { - // TODO: Why we need attachedTo to be found? It uses attachedTo._class, attachedTo.space only inside - // We found case for Todos, we could attach a collection with - const attachedTo = (await findAll(ctx, _class, { _id }, { limit: 1 }))[0] - if (attachedTo !== undefined) { - result.push( - this.getCollectionUpdateTx( - _id, - _class, - tx.modifiedBy, - colTx.modifiedOn, - attachedTo, // { _class: colTx.objectClass, space: colTx.objectSpace }, - { - $inc: { [colTx.collection]: isCreateTx ? 1 : -1 } - } - ) - ) - } - } - } - } - return result - } - - private getParentClass (_class: Ref>): Ref> { - const baseDomain = this.context.hierarchy.getDomain(_class) - const ancestors = this.context.hierarchy.getAncestors(_class) - let result: Ref> = _class - for (const ancestor of ancestors) { - try { - const domain = this.context.hierarchy.getClass(ancestor).domain - if (domain === baseDomain) { - result = ancestor - } - } catch {} - } - return result - } - - private getMixins (_class: Ref>, object: Doc): Array>> { - const parentClass = this.getParentClass(_class) - const descendants = this.context.hierarchy.getDescendants(parentClass) - return descendants.filter( - (m) => - this.context.hierarchy.getClass(m).kind === ClassifierKind.MIXIN && this.context.hierarchy.hasMixin(object, m) - ) - } - - private deleteObject (ctx: MeasureContext, object: Doc, removedMap: Map, Doc>): Tx[] { - const result: Tx[] = [] - const factory = new TxFactory(object.modifiedBy, true) - if (this.context.hierarchy.isDerived(object._class, core.class.AttachedDoc)) { - const adoc = object as AttachedDoc - const nestedTx = factory.createTxRemoveDoc(adoc._class, adoc.space, adoc._id) - const tx = factory.createTxCollectionCUD( - adoc.attachedToClass, - adoc.attachedTo, - adoc.space, - adoc.collection, - nestedTx - ) - removedMap.set(adoc._id, adoc) - result.push(tx) - } else { - result.push(factory.createTxRemoveDoc(object._class, object.space, object._id)) - removedMap.set(object._id, object) - } - return result - } - - private async deleteRelatedDocuments ( - ctx: MeasureContext, - object: Doc, - findAll: SessionFindAll - ): Promise { - const result: Tx[] = [] - const objectClass = this.context.hierarchy.getClass(object._class) - if (this.context.hierarchy.hasMixin(objectClass, serverCore.mixin.ObjectDDParticipant)) { - const removeParticipand: ObjectDDParticipant = this.context.hierarchy.as( - objectClass, - serverCore.mixin.ObjectDDParticipant - ) - const collector = await getResource(removeParticipand.collectDocs) - const docs = await collector(object, this.context.hierarchy, (_class, query, options) => { - return findAll(ctx, _class, query, options) - }) - for (const d of docs) { - result.push(...this.deleteObject(ctx, d, ctx.contextData.removedMap)) - } - } - return result - } - - private async processMove (ctx: MeasureContext, txes: Tx[], findAll: SessionFindAll): Promise { - const result: Tx[] = [] - for (const tx of txes) { - if (!this.context.hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { - continue - } - const rtx = tx as TxUpdateDoc - if (rtx.operations.space === undefined || rtx.operations.space === rtx.objectSpace) { - continue - } - const factory = new TxFactory(tx.modifiedBy, true) - for (const [, attribute] of this.context.hierarchy.getAllAttributes(rtx.objectClass)) { - if (!this.context.hierarchy.isDerived(attribute.type._class, core.class.Collection)) { - continue - } - const collection = attribute.type as Collection - const allAttached = await findAll(ctx, collection.of, { attachedTo: rtx.objectId, space: rtx.objectSpace }) - const allTx = allAttached.map(({ _class, space, _id }) => - factory.createTxUpdateDoc(_class, space, _id, { space: rtx.operations.space }) - ) - result.push(...allTx) - } - } - return result - } -} diff --git a/server/middleware/src/txPush.ts b/server/middleware/src/txPush.ts deleted file mode 100644 index 694d7b8307c..00000000000 --- a/server/middleware/src/txPush.ts +++ /dev/null @@ -1,100 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - DOMAIN_TRANSIENT, - DOMAIN_TX, - generateId, - TxProcessor, - WorkspaceEvent, - type Doc, - type MeasureContext, - type SessionData, - type Tx, - type TxCUD, - type TxResult, - type TxWorkspaceEvent -} from '@hcengineering/core' -import { PlatformError, unknownError } from '@hcengineering/platform' -import type { DBAdapterManager, Middleware, PipelineContext, TxMiddlewareResult } from '@hcengineering/server-core' -import { BaseMiddleware } from '@hcengineering/server-core' - -/** - * Will store transactions to tx adapter - * @public - */ -export class TxMiddleware extends BaseMiddleware implements Middleware { - adapterManager!: DBAdapterManager - static async create (ctx: MeasureContext, context: PipelineContext, next?: Middleware): Promise { - const middleware = new TxMiddleware(context, next) - if (context.adapterManager == null) { - throw new PlatformError(unknownError('Adapter manager should be specified')) - } - middleware.adapterManager = context.adapterManager - return middleware - } - - async init (ctx: MeasureContext): Promise {} - - async tx (ctx: MeasureContext, txes: Tx[]): Promise { - const txToStore: Tx[] = [] - for (const tx of txes) { - if (TxProcessor.isExtendsCUD(tx._class)) { - if (tx.space !== core.space.DerivedTx || tx.objectSpace === core.space.Model) { - const objectClass = (tx as TxCUD).objectClass - if ( - objectClass !== core.class.BenchmarkDoc && - this.context.hierarchy.findDomain(objectClass) !== DOMAIN_TRANSIENT - ) { - const { meta, ...txData } = tx - txToStore.push(txData) - } - } - } - } - let txPromise: Promise | undefined - if (txToStore.length > 0) { - txPromise = ctx.with( - 'tx-push', - {}, - (ctx) => this.adapterManager.getAdapter(DOMAIN_TX, true).tx(ctx, ...txToStore), - { - count: txToStore.length, - txes: Array.from(new Set(txToStore.map((it) => it._class))) - } - ) - // We need to remember last Tx Id in context, so it will be used during reconnect to track a requirement for refresh. - this.context.lastTx = txToStore[txToStore.length - 1]._id - // We need to deliver information to all clients so far. - const evt: TxWorkspaceEvent = { - _class: core.class.TxWorkspaceEvent, - _id: generateId(), - event: WorkspaceEvent.LastTx, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - objectSpace: core.space.DerivedTx, - space: core.space.DerivedTx, - params: { - lastTx: this.context.lastTx - } - } - ;(ctx.contextData as SessionData).broadcast.txes.push(evt) - } - if (txPromise !== undefined) { - await txPromise - } - return await this.provideTx(ctx, txes) - } -} diff --git a/server/middleware/src/userStatus.ts b/server/middleware/src/userStatus.ts deleted file mode 100644 index bc2f33fcb8c..00000000000 --- a/server/middleware/src/userStatus.ts +++ /dev/null @@ -1,84 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import core, { - type MeasureContext, - type Tx, - type SessionData, - type TxApplyIf, - type TxCUD, - type Doc, - TxProcessor, - type UserStatus, - type TxCreateDoc, - type TxUpdateDoc, - type Ref -} from '@hcengineering/core' -import { - BaseMiddleware, - type Middleware, - type TxMiddlewareResult, - type PipelineContext -} from '@hcengineering/server-core' - -/** - * @public - */ -export class UserStatusMiddleware extends BaseMiddleware implements Middleware { - private constructor (context: PipelineContext, next?: Middleware) { - super(context, next) - } - - static async create ( - _: MeasureContext, - context: PipelineContext, - next: Middleware | undefined - ): Promise { - return new UserStatusMiddleware(context, next) - } - - tx (ctx: MeasureContext, txes: Tx[]): Promise { - for (const tx of txes) { - if (tx._class === core.class.TxApplyIf) { - const atx = tx as TxApplyIf - atx.txes.forEach((it) => { - this.processTx(it) - }) - } else { - this.processTx(tx as TxCUD) - } - } - return this.provideTx(ctx, txes) - } - - private processTx (tx: TxCUD): void { - if (tx._class === core.class.TxCreateDoc && tx.objectClass === core.class.UserStatus) { - const status = TxProcessor.createDoc2Doc(tx as TxCreateDoc) - const map = this.context.userStatusMap ?? new Map() - map.set(status._id, { online: status.online, user: status.user }) - this.context.userStatusMap = map - } else if (tx._class === core.class.TxUpdateDoc && tx.objectClass === core.class.UserStatus) { - const uTx = tx as TxUpdateDoc - if ('online' in uTx.operations) { - const current = this.context.userStatusMap?.get(uTx.objectId) - const online = uTx.operations.online - if (current !== undefined && online !== undefined) { - this.context.userStatusMap?.set(uTx.objectId, { online, user: current.user }) - } - } - } else if (tx._class === core.class.TxRemoveDoc && tx.objectClass === core.class.UserStatus) { - this.context.userStatusMap?.delete(tx.objectId as Ref) - } - } -} diff --git a/server/middleware/src/utils.ts b/server/middleware/src/utils.ts deleted file mode 100644 index b6ea5b5c7f9..00000000000 --- a/server/middleware/src/utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type Account, - AccountRole, - systemAccountUuid, - TxProcessor, - type Doc, - type Hierarchy, - type MeasureContext, - type SessionData, - type Tx, - type TxCUD -} from '@hcengineering/core' - -export function isOwner (account: Account, ctx: MeasureContext): boolean { - return account.role === AccountRole.Owner || isSystem(account, ctx) -} - -export function isSystem (account: Account, ctx: MeasureContext): boolean { - return account.uuid === systemAccountUuid -} - -export function filterBroadcastOnly (tx: Tx[], hierarchy: Hierarchy): Tx[] { - const ftx = tx.filter((it) => { - if (TxProcessor.isExtendsCUD(it._class)) { - const cud = it as TxCUD - const bonly = hierarchy.getClassifierProp(cud.objectClass, 'broadcastOnly') - if (bonly === true) { - return false - } - try { - const objClass = hierarchy.getClass(cud.objectClass) - const mix = hierarchy.hasMixin(objClass, core.mixin.TransientConfiguration) - if (mix && hierarchy.as(objClass, core.mixin.TransientConfiguration).broadcastOnly) { - hierarchy.setClassifierProp(cud.objectClass, 'broadcastOnly', true) - // We do not need to store a broadcast only transactions into model. - return false - } - } catch { - return true - } - } - return true - }) - return ftx -} diff --git a/server/middleware/tsconfig.json b/server/middleware/tsconfig.json deleted file mode 100644 index c6a877cf6c3..00000000000 --- a/server/middleware/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/minio/.eslintrc.js b/server/minio/.eslintrc.js deleted file mode 100644 index ce90fb9646f..00000000000 --- a/server/minio/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/minio/.npmignore b/server/minio/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/minio/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/minio/config/rig.json b/server/minio/config/rig.json deleted file mode 100644 index 78cc5a17334..00000000000 --- a/server/minio/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/server/minio/jest.config.js b/server/minio/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/minio/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/minio/package.json b/server/minio/package.json deleted file mode 100644 index e249b9745f0..00000000000 --- a/server/minio/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@hcengineering/minio", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "@types/node": "^22.15.29", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" - }, - "dependencies": { - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "minio": "^8.0.5" - } -} diff --git a/server/minio/src/__tests__/minio.test.ts b/server/minio/src/__tests__/minio.test.ts deleted file mode 100644 index 3193395292a..00000000000 --- a/server/minio/src/__tests__/minio.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { MeasureMetricsContext, type WorkspaceDataId, type WorkspaceUuid, generateId } from '@hcengineering/core' -import { objectsToArray, type StorageConfiguration } from '@hcengineering/server-core' -import { MinioService, processConfigFromEnv, type MinioConfig } from '..' - -describe('minio operations', () => { - const config: StorageConfiguration = { default: 'minio', storages: [] } - const minioConfigVar = processConfigFromEnv(config) - if (minioConfigVar !== undefined || config.storages[0] === undefined) { - console.error('No Minio config env is configured:' + minioConfigVar) - it.skip('No Minio config env is configured', async () => {}) - return - } - const toolCtx = new MeasureMetricsContext('test', {}) - it('check root bucket', async () => { - jest.setTimeout(50000) - const minioService = new MinioService({ ...(config.storages[0] as MinioConfig), rootBucket: 'test-bucket' }) - - let existingTestBuckets = await minioService.listBuckets(toolCtx) - // Delete old buckets - for (const b of existingTestBuckets) { - await b.delete() - } - - const genWorkspaceId1 = generateId() as unknown as WorkspaceDataId - const genWorkspaceId2 = generateId() as unknown as WorkspaceDataId - - expect(genWorkspaceId1).not.toEqual(genWorkspaceId2) - - const wsIds1 = { - uuid: genWorkspaceId1 as unknown as WorkspaceUuid, - dataId: genWorkspaceId1, - url: '' - } - const wsIds2 = { - uuid: genWorkspaceId2 as unknown as WorkspaceUuid, - dataId: genWorkspaceId2, - url: '' - } - await minioService.make(toolCtx, wsIds1) - await minioService.make(toolCtx, wsIds2) - - const v1 = generateId() - await minioService.put(toolCtx, wsIds1, 'obj1.txt', v1, 'text/plain') - await minioService.put(toolCtx, wsIds2, 'obj2.txt', v1, 'text/plain') - - const w1Objects = await objectsToArray(toolCtx, minioService, wsIds1) - expect(w1Objects.map((it) => it._id)).toEqual(['obj1.txt']) - - const w2Objects = await objectsToArray(toolCtx, minioService, wsIds2) - expect(w2Objects.map((it) => it._id)).toEqual(['obj2.txt']) - - await minioService.put(toolCtx, wsIds1, 'obj1.txt', 'obj1', 'text/plain') - await minioService.put(toolCtx, wsIds1, 'obj2.txt', 'obj2', 'text/plain') - - const w1Objects2 = await objectsToArray(toolCtx, minioService, wsIds1) - expect(w1Objects2.map((it) => it._id)).toEqual(['obj1.txt', 'obj2.txt']) - - const data = Buffer.concat(await minioService.read(toolCtx, wsIds1, 'obj1.txt')) - - expect('obj1').toEqual(data.toString()) - - existingTestBuckets = await minioService.listBuckets(toolCtx) - expect(existingTestBuckets.length).toEqual(2) - // Delete old buckets - for (const b of existingTestBuckets) { - await b.delete() - } - existingTestBuckets = await minioService.listBuckets(toolCtx) - expect(existingTestBuckets.length).toEqual(0) - }) -}) diff --git a/server/minio/src/index.ts b/server/minio/src/index.ts deleted file mode 100644 index 02b15f0c707..00000000000 --- a/server/minio/src/index.ts +++ /dev/null @@ -1,422 +0,0 @@ -// -// 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 { Client, type BucketItem, type BucketStream } from 'minio' - -import core, { - withContext, - type WorkspaceIds, - type WorkspaceDataId, - type Blob, - type MeasureContext, - type Ref, - type WorkspaceUuid -} from '@hcengineering/core' -import { getMetadata } from '@hcengineering/platform' -import serverCore, { - removeAllObjects, - getDataId, - type BlobStorageIterator, - type BucketInfo, - type ListBlobResult, - type StorageAdapter, - type StorageConfig, - type StorageConfiguration, - type UploadedObjectInfo -} from '@hcengineering/server-core' -import { type Readable } from 'stream' - -export interface MinioConfig extends StorageConfig { - kind: 'minio' - accessKey: string - secretKey: string - useSSL?: string - region?: string - - // If defined, all resources will be inside selected root bucket. - rootBucket?: string - - // A prefix string to be added to a bucketId in case rootBucket not used - bucketPrefix?: string -} - -export const CONFIG_KIND = 'minio' - -/** - * @public - */ -export class MinioService implements StorageAdapter { - client: Client - constructor (readonly opt: MinioConfig) { - this.client = new Client({ - endPoint: opt.endpoint, - accessKey: opt.accessKey, - secretKey: opt.secretKey, - region: opt.region ?? 'us-east-1', - port: opt.port ?? 9000, - useSSL: opt.useSSL === 'true' - }) - } - - async initialize (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} - - /** - * @public - */ - getBucketId (wsIds: WorkspaceIds): string { - return this.opt.rootBucket ?? (this.opt.bucketPrefix ?? '') + getDataId(wsIds) - } - - getBucketFolder (wsIds: WorkspaceIds): string { - return getDataId(wsIds) - } - - async close (): Promise {} - async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - return await this.client.bucketExists(this.getBucketId(wsIds)) - } - - @withContext('make') - async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - try { - await this.client.makeBucket(this.getBucketId(wsIds), this.opt.region ?? 'us-east-1') - } catch (err: any) { - if (err.code === 'BucketAlreadyOwnedByYou') { - return - } - throw err - } - } - - async listBuckets (ctx: MeasureContext): Promise { - if (this.opt.rootBucket !== undefined) { - const info = new Map() - if (!(await this.client.bucketExists(this.opt.rootBucket))) { - return [] - } - const stream = this.client.listObjectsV2(this.opt.rootBucket, '', false) - await new Promise((resolve, reject) => { - stream.on('end', () => { - stream.destroy() - resolve() - }) - stream.on('error', (err) => { - console.error(err) - stream?.destroy() - reject(err) - }) - stream.on('data', (data) => { - const wsDataId = data.prefix?.split('/')?.[0] as WorkspaceDataId - if (wsDataId !== undefined && !info.has(wsDataId)) { - const wsIds = { - uuid: wsDataId as unknown as WorkspaceUuid, - dataId: wsDataId, - url: '' - } - info.set(wsDataId, { - name: wsDataId, - delete: async () => { - await this.delete(ctx, wsIds) - }, - list: async () => await this.listStream(ctx, wsIds) - }) - } - }) - }) - stream.destroy() - return Array.from(info.values()) - } else { - const productPostfix = this.getBucketFolder({ uuid: '' as WorkspaceUuid, dataId: '' as WorkspaceDataId, url: '' }) - const buckets = await this.client.listBuckets() - return buckets - .filter((it) => it.name.endsWith(productPostfix)) - .map((it) => { - let name = it.name as WorkspaceDataId - name = name.slice(0, name.length - productPostfix.length) as WorkspaceDataId - const wsIds = { - uuid: name as unknown as WorkspaceUuid, - dataId: name, - url: '' - } - return { - name, - delete: async () => { - await this.delete(ctx, wsIds) - }, - list: async () => await this.listStream(ctx, wsIds) - } - }) - } - } - - getDocumentKey (wsIds: WorkspaceIds, name: string): string { - return this.opt.rootBucket === undefined ? name : `${this.getBucketFolder(wsIds)}/${name}` - } - - @withContext('remove') - async remove (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]): Promise { - const toRemove = objectNames.map((it) => this.getDocumentKey(wsIds, it)) - await this.client.removeObjects(this.getBucketId(wsIds), toRemove) - } - - @withContext('delete') - async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - try { - await removeAllObjects(ctx, this, wsIds) - } catch (err: any) { - ctx.error('failed to clean all objects', { error: err }) - } - if (this.opt.rootBucket === undefined) { - // Also delete a bucket - await this.client.removeBucket(this.getBucketId(wsIds)) - } - } - - stripPrefix (prefix: string | undefined, key: string): string { - if (prefix !== undefined && key.startsWith(prefix)) { - return key.slice(prefix.length) - } - return key - } - - rootPrefix (wsIds: WorkspaceIds): string | undefined { - return this.opt.rootBucket !== undefined ? this.getBucketFolder(wsIds) + '/' : undefined - } - - @withContext('listStream') - async listStream (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - let hasMore = true - let stream: BucketStream | undefined - let done = false - let error: Error | undefined - let onNext: () => void = () => {} - const buffer: ListBlobResult[] = [] - - const rootPrefix = this.rootPrefix(wsIds) - return { - next: async (): Promise => { - try { - if (stream === undefined && !done) { - const rprefix = rootPrefix ?? '' - stream = this.client.listObjectsV2(this.getBucketId(wsIds), rprefix, true) - stream.on('end', () => { - stream?.destroy() - done = true - stream = undefined - hasMore = false - onNext() - }) - stream.on('error', (err) => { - stream?.destroy() - stream = undefined - error = err - hasMore = false - onNext() - }) - stream.on('data', (data) => { - if (data.name !== undefined) { - const _id = this.stripPrefix(rootPrefix, data.name) - buffer.push({ - _id: _id as Ref, - _class: core.class.Blob, - etag: data.etag, - size: data.size, - provider: this.opt.name, - space: core.space.Configuration, - modifiedBy: core.account.System, - modifiedOn: data.lastModified.getTime() - }) - } - onNext() - if (buffer.length > 100) { - stream?.pause() - } - }) - } - } catch (err: any) { - const msg = (err?.message as string) ?? '' - if (msg.includes('Invalid bucket name') || msg.includes('The specified bucket does not exist')) { - hasMore = false - return [] - } - error = err - } - - if (buffer.length > 0) { - return buffer.splice(0, 50) - } - if (!hasMore) { - return [] - } - return await new Promise((resolve, reject) => { - onNext = () => { - if (error != null) { - reject(error) - } - onNext = () => {} - resolve(buffer.splice(0, 50)) - } - stream?.resume() - }) - }, - close: async () => { - stream?.destroy() - } - } - } - - @withContext('stat') - async stat (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - try { - const result = await this.client.statObject(this.getBucketId(wsIds), this.getDocumentKey(wsIds, objectName)) - const rootPrefix = this.rootPrefix(wsIds) - return { - provider: '', - _class: core.class.Blob, - _id: this.stripPrefix(rootPrefix, objectName) as Ref, - contentType: result.metaData['content-type'], - size: result.size, - etag: result.etag, - space: core.space.Configuration, - modifiedBy: core.account.System, - modifiedOn: result.lastModified.getTime(), - version: result.versionId ?? null - } - } catch (err: any) { - if ( - err?.code === 'NoSuchKey' || - err?.code === 'NotFound' || - err?.message === 'No such key' || - err?.Code === 'NoSuchKey' - ) { - // Do not print error in this case - return - } - - ctx.error('failed to stat object', { error: err, objectName, wsIds }) - throw err - } - } - - @withContext('get') - async get (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - return await this.client.getObject(this.getBucketId(wsIds), this.getDocumentKey(wsIds, objectName)) - } - - @withContext('put') - async put ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - stream: Readable | Buffer | string, - contentType: string, - size?: number - ): Promise { - return await this.client.putObject(this.getBucketId(wsIds), this.getDocumentKey(wsIds, objectName), stream, size, { - 'Content-Type': contentType - }) - } - - @withContext('read') - async read (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - const data = await this.client.getObject(this.getBucketId(wsIds), this.getDocumentKey(wsIds, objectName)) - const chunks: Buffer[] = [] - - await new Promise((resolve, reject) => { - data.on('readable', () => { - let chunk - while ((chunk = data.read()) !== null) { - const b = chunk as Buffer - chunks.push(b) - } - }) - - data.on('end', () => { - data.destroy() - resolve(null) - }) - data.on('error', (err) => { - reject(err) - }) - }) - return chunks - } - - @withContext('partial') - async partial ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - offset: number, - length?: number - ): Promise { - return await this.client.getPartialObject( - this.getBucketId(wsIds), - this.getDocumentKey(wsIds, objectName), - offset, - length - ) - } - - @withContext('getUrl') - async getUrl (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - const filesUrl = getMetadata(serverCore.metadata.FilesUrl) ?? '' - return filesUrl.replaceAll(':workspace', getDataId(wsIds)).replaceAll(':blobId', objectName) - } -} - -export function processConfigFromEnv (storageConfig: StorageConfiguration): string | undefined { - let minioEndpoint = process.env.MINIO_ENDPOINT - if (minioEndpoint === undefined) { - return 'MINIO_ENDPOINT' - } - const minioAccessKey = process.env.MINIO_ACCESS_KEY - if (minioAccessKey === undefined) { - return 'MINIO_ACCESS_KEY' - } - - let minioPort = 9000 - const sp = minioEndpoint.split(':') - if (sp.length > 1) { - minioEndpoint = sp[0] - minioPort = parseInt(sp[1]) - } - - const minioSecretKey = process.env.MINIO_SECRET_KEY - if (minioSecretKey === undefined) { - return 'MINIO_SECRET_KEY' - } - - const minioConfig: MinioConfig = { - kind: 'minio', - name: 'minio', - port: minioPort, - region: 'us-east-1', - useSSL: 'false', - endpoint: minioEndpoint, - accessKey: minioAccessKey, - secretKey: minioSecretKey - } - storageConfig.storages.push(minioConfig) - storageConfig.default = 'minio' -} - -export function addMinioFallback (storageConfig: StorageConfiguration): void { - const required = processConfigFromEnv(storageConfig) - if (required !== undefined) { - console.error(`Required ${required} env to be configured`) - process.exit(1) - } -} diff --git a/server/minio/tsconfig.json b/server/minio/tsconfig.json deleted file mode 100644 index c6a877cf6c3..00000000000 --- a/server/minio/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/mongo/.eslintrc.js b/server/mongo/.eslintrc.js deleted file mode 100644 index ce90fb9646f..00000000000 --- a/server/mongo/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/mongo/.npmignore b/server/mongo/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/mongo/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/mongo/CHANGELOG.json b/server/mongo/CHANGELOG.json deleted file mode 100644 index 1ceecbb3047..00000000000 --- a/server/mongo/CHANGELOG.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@hcengineering/mongo", - "entries": [ - { - "version": "0.7.0", - "tag": "@hcengineering/mongo_v0.6.1", - "date": "Fri, 20 Aug 2021 16:21:03 GMT", - "comments": { - "patch": [ - { - "comment": "Transaction ordering" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `~0.6.10` to `~0.6.11`" - } - ] - } - } - ] -} diff --git a/server/mongo/CHANGELOG.md b/server/mongo/CHANGELOG.md deleted file mode 100644 index d4e7573f037..00000000000 --- a/server/mongo/CHANGELOG.md +++ /dev/null @@ -1,11 +0,0 @@ -# Change Log - @hcengineering/mongo - -This log was last generated on Fri, 20 Aug 2021 16:21:03 GMT and should not be manually modified. - -## 0.6.1 -Fri, 20 Aug 2021 16:21:03 GMT - -### Patches - -- Transaction ordering - diff --git a/server/mongo/config/rig.json b/server/mongo/config/rig.json deleted file mode 100644 index 78cc5a17334..00000000000 --- a/server/mongo/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/server/mongo/jest.config.js b/server/mongo/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/mongo/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/mongo/package.json b/server/mongo/package.json deleted file mode 100644 index 99033d85c8d..00000000000 --- a/server/mongo/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@hcengineering/mongo", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/node": "^22.15.29" - }, - "dependencies": { - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "mongodb": "^6.16.0", - "bson": "^6.10.3" - } -} diff --git a/server/mongo/src/__tests__/minmodel.ts b/server/mongo/src/__tests__/minmodel.ts deleted file mode 100644 index be5c1b76553..00000000000 --- a/server/mongo/src/__tests__/minmodel.ts +++ /dev/null @@ -1,234 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type PersonId, - type Arr, - type AttachedDoc, - type Class, - ClassifierKind, - type Data, - type Doc, - DOMAIN_MODEL, - DOMAIN_RELATION, - DOMAIN_TX, - type Mixin, - type Obj, - type Ref, - type TxCreateDoc, - type TxCUD, - TxFactory, - type AccountUuid -} from '@hcengineering/core' -import type { IntlString, Plugin } from '@hcengineering/platform' -import { plugin } from '@hcengineering/platform' -import { taskPlugin } from './tasks' - -export const txFactory = new TxFactory(core.account.System) - -export function createClass (_class: Ref>, attributes: Data>): TxCreateDoc { - return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class) -} - -/** - * @public - */ -export function createDoc ( - _class: Ref>, - attributes: Data, - id?: Ref, - modifiedBy?: PersonId -): TxCreateDoc { - const result = txFactory.createTxCreateDoc(_class, core.space.Model, attributes, id) - if (modifiedBy !== undefined) { - result.modifiedBy = modifiedBy - } - return result -} - -/** - * @public - */ -export interface TestMixin extends Doc { - arr: Arr -} - -/** - * @public - */ -export interface AttachedComment extends AttachedDoc { - message: string -} - -/** - * @public - */ -export const test = plugin('test' as Plugin, { - mixin: { - TestMixin: '' as Ref> - }, - class: { - TestComment: '' as Ref> - } -}) - -/** - * @public - * Generate minimal model for testing purposes. - * @returns R - */ -export function genMinModel (): TxCUD[] { - const txes = [] - // Fill Tx'es with basic model classes. - txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS })) - txes.push( - createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS }) - ) - txes.push( - createClass(core.class.Relation, { - label: 'Relation' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_RELATION - }) - ) - txes.push( - createClass(core.class.Association, { - label: 'Association' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - txes.push( - createClass(core.class.AttachedDoc, { - label: 'AttachedDoc' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.MIXIN - }) - ) - txes.push( - createClass(core.class.Class, { - label: 'Class' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - txes.push( - createClass(core.class.Space, { - label: 'Space' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - - txes.push( - createClass(core.class.Tx, { - label: 'Tx' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCUD, { - label: 'TxCUD' as IntlString, - extends: core.class.Tx, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCreateDoc, { - label: 'TxCreateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxUpdateDoc, { - label: 'TxUpdateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxRemoveDoc, { - label: 'TxRemoveDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - - txes.push( - createClass(test.mixin.TestMixin, { - label: 'TestMixin' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.MIXIN - }) - ) - - txes.push( - createClass(test.class.TestComment, { - label: 'TestComment' as IntlString, - extends: core.class.AttachedDoc, - kind: ClassifierKind.CLASS - }) - ) - - const u1 = 'User1' as AccountUuid - const u2 = 'User2' as AccountUuid - txes.push( - createDoc(core.class.Space, { - name: 'Sp1', - description: '', - private: false, - archived: false, - members: [u1, u2] - }) - ) - - txes.push( - createDoc(core.class.Space, { - name: 'Sp2', - description: '', - private: false, - archived: false, - members: [u1] - }) - ) - - txes.push( - createClass(core.class.DomainIndexConfiguration, { - label: 'DomainIndexConfiguration' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - - txes.push( - createDoc(core.class.Association, { - nameA: 'my-assoc', - nameB: 'my-assoc', - classA: taskPlugin.class.Task, - classB: taskPlugin.class.Task, - type: '1:1' - }) - ) - return txes -} diff --git a/server/mongo/src/__tests__/storage.test.ts b/server/mongo/src/__tests__/storage.test.ts deleted file mode 100644 index 9bfb163073e..00000000000 --- a/server/mongo/src/__tests__/storage.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import core, { - type Client, - createClient, - Hierarchy, - MeasureMetricsContext, - ModelDb, - type Ref, - SortingOrder, - type Space, - TxOperations, - type WorkspaceUuid -} from '@hcengineering/core' -import { type DbAdapter, wrapAdapterToClient } from '@hcengineering/server-core' -import { createMongoAdapter, createMongoTxAdapter } from '..' -import { getMongoClient, type MongoClientReference, shutdownMongo } from '../utils' -import { genMinModel } from './minmodel' -import { createTaskModel, type Task, type TaskComment, taskPlugin } from './tasks' - -const txes = genMinModel() - -createTaskModel(txes) - -describe('mongo operations', () => { - const mongodbUri: string = process.env.MONGO_URL ?? 'mongodb://localhost:27017' - let mongoClient!: MongoClientReference - let dbUuid = crypto.randomUUID() as WorkspaceUuid - let hierarchy: Hierarchy - let model: ModelDb - let client: Client - let operations: TxOperations - let serverStorage: DbAdapter - - beforeAll(async () => { - mongoClient = getMongoClient(mongodbUri) - }) - - afterAll(async () => { - mongoClient.close() - await shutdownMongo() - }) - - beforeEach(async () => { - dbUuid = crypto.randomUUID() as WorkspaceUuid - await initDb() - }) - - afterEach(async () => { - try { - await (await mongoClient.getClient()).db(dbUuid).dropDatabase() - } catch (eee) {} - await serverStorage.close() - }) - - async function initDb (): Promise { - // Remove all stuff from database. - hierarchy = new Hierarchy() - model = new ModelDb(hierarchy) - for (const t of txes) { - hierarchy.tx(t) - } - for (const t of txes) { - await model.tx(t) - } - - const mctx = new MeasureMetricsContext('', {}) - const txStorage = await createMongoTxAdapter( - new MeasureMetricsContext('', {}), - hierarchy, - mongodbUri, - { - uuid: dbUuid, - url: dbUuid - }, - model - ) - - serverStorage = await createMongoAdapter( - new MeasureMetricsContext('', {}), - hierarchy, - mongodbUri, - { - uuid: dbUuid, - url: dbUuid - }, - model - ) - - // Put all transactions to Tx - for (const t of txes) { - await txStorage.tx(mctx, t) - } - - await txStorage.close() - - const ctx = new MeasureMetricsContext('client', {}) - - client = await createClient(async (handler) => { - return wrapAdapterToClient(ctx, serverStorage, txes) - }) - - operations = new TxOperations(client, core.account.System) - } - - it('check add', async () => { - const times: number[] = [] - for (let i = 0; i < 50; i++) { - const t = Date.now() - await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: `my-task-${i}`, - description: `${i * i}`, - rate: 20 + i - }) - times.push(Date.now() - t) - } - - console.log('createDoc times', times) - - const r = await client.findAll(taskPlugin.class.Task, {}) - expect(r.length).toEqual(50) - }) - - it('check find by criteria', async () => { - for (let i = 0; i < 50; i++) { - await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: `my-task-${i}`, - description: `${i * i}`, - rate: 20 + i - }) - } - - const r = await client.findAll(taskPlugin.class.Task, {}) - expect(r.length).toEqual(50) - - const first = await client.findAll(taskPlugin.class.Task, { name: 'my-task-0' }) - expect(first.length).toEqual(1) - - const second = await client.findAll(taskPlugin.class.Task, { name: { $like: '%0' } }) - expect(second.length).toEqual(5) - - const third = await client.findAll(taskPlugin.class.Task, { rate: { $in: [25, 26, 27, 28] } }) - expect(third.length).toEqual(4) - }) - - it('check update', async () => { - await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: 'my-task', - description: 'some data ', - rate: 20 - }) - - const doc = (await client.findAll(taskPlugin.class.Task, {}))[0] - - await operations.updateDoc(doc._class, doc.space, doc._id, { rate: 30 }) - const tasks = await client.findAll(taskPlugin.class.Task, {}) - expect(tasks.length).toEqual(1) - expect(tasks[0].rate).toEqual(30) - }) - - it('check remove', async () => { - for (let i = 0; i < 10; i++) { - await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: `my-task-${i}`, - description: `${i * i}`, - rate: 20 + i - }) - } - - let r = await client.findAll(taskPlugin.class.Task, {}) - expect(r.length).toEqual(10) - await operations.removeDoc(taskPlugin.class.Task, '' as Ref, r[0]._id) - r = await client.findAll(taskPlugin.class.Task, {}) - expect(r.length).toEqual(9) - }) - - it('limit and sorting', async () => { - for (let i = 0; i < 5; i++) { - await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: `my-task-${i}`, - description: `${i * i}`, - rate: 20 + i - }) - } - - const without = await client.findAll(taskPlugin.class.Task, {}) - expect(without).toHaveLength(5) - - const limit = await client.findAll(taskPlugin.class.Task, {}, { limit: 1 }) - expect(limit).toHaveLength(1) - - const sortAsc = await client.findAll(taskPlugin.class.Task, {}, { sort: { name: SortingOrder.Ascending } }) - expect(sortAsc[0].name).toMatch('my-task-0') - - const sortDesc = await client.findAll(taskPlugin.class.Task, {}, { sort: { name: SortingOrder.Descending } }) - expect(sortDesc[0].name).toMatch('my-task-4') - }) - - it('check attached', async () => { - const docId = await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: 'my-task', - description: 'Descr', - rate: 20 - }) - - const commentId = await operations.addCollection( - taskPlugin.class.TaskComment, - '' as Ref, - docId, - taskPlugin.class.Task, - 'tasks', - { - message: 'my-msg', - date: new Date() - } - ) - - await operations.addCollection( - taskPlugin.class.TaskComment, - '' as Ref, - docId, - taskPlugin.class.Task, - 'tasks', - { - message: 'my-msg2', - date: new Date() - } - ) - - const r2 = await client.findAll( - taskPlugin.class.TaskComment, - {}, - { - lookup: { - attachedTo: taskPlugin.class.Task - } - } - ) - expect(r2.length).toEqual(2) - expect((r2[0].$lookup?.attachedTo as Task)?._id).toEqual(docId) - - const r3 = await client.findAll( - taskPlugin.class.Task, - {}, - { - lookup: { - _id: { comment: taskPlugin.class.TaskComment } - } - } - ) - - expect(r3).toHaveLength(1) - expect((r3[0].$lookup as any).comment).toHaveLength(2) - - const comment2Id = await operations.addCollection( - taskPlugin.class.TaskComment, - '' as Ref, - commentId, - taskPlugin.class.TaskComment, - 'comments', - { - message: 'my-msg3', - date: new Date() - } - ) - - const r4 = await client.findAll( - taskPlugin.class.TaskComment, - { - _id: comment2Id - }, - { - lookup: { attachedTo: [taskPlugin.class.TaskComment, { attachedTo: taskPlugin.class.Task } as any] } - } - ) - expect((r4[0].$lookup?.attachedTo as TaskComment)?._id).toEqual(commentId) - expect(((r4[0].$lookup?.attachedTo as any)?.$lookup.attachedTo as Task)?._id).toEqual(docId) - }) - - it('check associations', async () => { - const association = await operations.findOne(core.class.Association, {}) - if (association == null) { - throw new Error('Association not found') - } - - const firstTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: 'my-task', - description: 'Descr', - rate: 20 - }) - - const secondTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: 'my-task2', - description: 'Descr', - rate: 20 - }) - - await operations.createDoc(core.class.Relation, '' as Ref, { - docA: firstTask, - docB: secondTask, - association: association._id - }) - - const r = await client.findAll( - taskPlugin.class.Task, - { _id: firstTask }, - { - associations: [[association._id, 1]] - } - ) - expect(r.length).toEqual(1) - expect((r[0].$associations?.[association._id][0] as unknown as Task)?._id).toEqual(secondTask) - }) -}) diff --git a/server/mongo/src/__tests__/tasks.ts b/server/mongo/src/__tests__/tasks.ts deleted file mode 100644 index 12997282f44..00000000000 --- a/server/mongo/src/__tests__/tasks.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - type AttachedDoc, - type Class, - ClassifierKind, - type Data, - type Doc, - type Domain, - type PersonId, - type Ref, - type Space, - type Tx -} from '@hcengineering/core' -import { type IntlString, plugin, type Plugin } from '@hcengineering/platform' -import { createClass } from './minmodel' - -export interface TaskComment extends AttachedDoc { - message: string - date: Date -} - -export enum TaskStatus { - Open, - Close, - Resolved = 100, - InProgress -} - -export enum TaskReproduce { - Always = 'always', - Rare = 'rare', - Sometimes = 'sometimes' -} - -export interface Task extends Doc { - name: string - description: string - rate?: number - status?: TaskStatus - reproduce?: TaskReproduce - eta?: TaskEstimate | null -} - -/** - * Define ROM and Estimated Time to arrival - */ -export interface TaskEstimate extends AttachedDoc { - rom: number // in hours - eta: number // in hours -} - -export interface TaskMixin extends Task { - textValue?: string -} - -export interface TaskWithSecond extends Task { - secondTask: string | null -} - -const taskIds = 'taskIds' as Plugin - -export const taskPlugin = plugin(taskIds, { - class: { - Task: '' as Ref>, - TaskEstimate: '' as Ref>, - TaskComment: '' as Ref> - } -}) - -/** - * Create a random task with name specified - * @param name - */ -export function createTask (name: string, rate: number, description: string): Data { - return { - name, - description, - rate - } -} - -export const doc1: Task = { - _id: 'd1' as Ref, - _class: taskPlugin.class.Task, - name: 'my-space', - description: 'some-value', - rate: 20, - modifiedBy: 'user' as PersonId, - modifiedOn: 10, - // createdOn: 10, - space: '' as Ref -} - -export function createTaskModel (txes: Tx[]): void { - txes.push( - createClass(taskPlugin.class.Task, { - kind: ClassifierKind.CLASS, - label: 'Task' as IntlString, - domain: 'test-task' as Domain - }), - createClass(taskPlugin.class.TaskEstimate, { - kind: ClassifierKind.CLASS, - label: 'Estimate' as IntlString, - domain: 'test-task' as Domain - }), - createClass(taskPlugin.class.TaskComment, { - kind: ClassifierKind.CLASS, - label: 'Comment' as IntlString, - domain: 'test-task' as Domain - }) - ) -} diff --git a/server/mongo/src/index.ts b/server/mongo/src/index.ts deleted file mode 100644 index 8f154860131..00000000000 --- a/server/mongo/src/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021, 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { WorkspaceDestroyAdapter } from '@hcengineering/server-core' -import { getMongoClient, getWorkspaceMongoDB } from './utils' - -export * from './storage' -export * from './utils' - -export function createMongoDestroyAdapter (url: string): WorkspaceDestroyAdapter { - return { - deleteWorkspace: async (ctx, workspace, dataId): Promise => { - const client = getMongoClient(url) - try { - await ctx.with('delete-workspace', {}, async () => { - const dbClient = await client.getClient() - const db = getWorkspaceMongoDB(dbClient, dataId ?? workspace) - await db.dropDatabase() - }) - } catch (err) { - console.error('Failed to delete workspace', err) - } finally { - client.close() - } - } - } -} diff --git a/server/mongo/src/storage.ts b/server/mongo/src/storage.ts deleted file mode 100644 index 5e67a6894ab..00000000000 --- a/server/mongo/src/storage.ts +++ /dev/null @@ -1,1797 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - DOMAIN_MODEL, - DOMAIN_MODEL_TX, - DOMAIN_RELATION, - DOMAIN_TX, - SortingOrder, - TxProcessor, - addOperation, - cutObjectArray, - escapeLikeForRegexp, - groupByArray, - isOperator, - matchQuery, - platformNow, - toFindResult, - withContext, - type AssociationQuery, - type Class, - type Doc, - type DocInfo, - type DocumentQuery, - type DocumentUpdate, - type Domain, - type Enum, - type EnumOf, - type FindOptions, - type FindResult, - type Hierarchy, - type Iterator, - type Lookup, - type MeasureContext, - type Mixin, - type ModelDb, - type Projection, - type QueryUpdate, - type Ref, - type ReverseLookups, - type SortQuerySelector, - type SortingQuery, - type SortingRules, - type StorageIterator, - type Tx, - type TxCUD, - type TxCreateDoc, - type TxMixin, - type TxRemoveDoc, - type TxResult, - type TxUpdateDoc, - type WithLookup, - type WorkspaceIds -} from '@hcengineering/core' -import { - calcHashHash, - type DbAdapter, - type DbAdapterHandler, - type DomainHelperOperations, - type RawFindIterator, - type ServerFindOptions, - type StorageAdapter, - type TxAdapter -} from '@hcengineering/server-core' -import { - ObjectId, - type AbstractCursor, - type AnyBulkWriteOperation, - type Collection, - type Db, - type Document, - type Filter, - type FindCursor, - type FindOptions as MongoFindOptions, - type Sort, - type UpdateFilter, - type WithId -} from 'mongodb' -import { DBCollectionHelper, getMongoClient, getWorkspaceMongoDB, type MongoClientReference } from './utils' - -function translateDoc (doc: Doc, hash: string): Doc { - return { ...doc, '%hash%': hash } as any -} - -function isLookupQuery (query: DocumentQuery): boolean { - for (const key in query) { - if (key.includes('$lookup.')) return true - } - return false -} - -function isLookupSort (sort: SortingQuery | undefined): boolean { - if (sort === undefined) return false - for (const key in sort) { - if (key.includes('$lookup.')) return true - } - return false -} - -interface LookupStep { - from: string - localField?: string - foreignField?: string - as: string - let?: any - pipeline?: any -} - -export async function toArray (cursor: AbstractCursor): Promise { - const data: T[] = [] - - while (true) { - const d = await cursor.next() - if (d === null) { - break - } - data.push(d) - const batch = cursor.readBufferedDocuments() - if (batch.length > 0) { - data.push(...batch) - } - } - await cursor.close() - return data -} - -export interface DbAdapterOptions { - calculateHash?: (doc: Doc) => { digest: string, size: number } -} - -abstract class MongoAdapterBase implements DbAdapter { - _db: DBCollectionHelper - - handlers: DbAdapterHandler[] = [] - - on (handler: DbAdapterHandler): void { - this.handlers.push(handler) - } - - handleEvent (domain: Domain, event: 'add' | 'update' | 'delete' | 'read', count: number): void { - for (const handler of this.handlers) { - handler(domain, event, count, this._db) - } - } - - constructor ( - protected readonly db: Db, - protected readonly hierarchy: Hierarchy, - protected readonly modelDb: ModelDb, - protected readonly client: MongoClientReference, - protected readonly options?: DbAdapterOptions - ) { - this._db = new DBCollectionHelper(db) - } - - async traverse( - domain: Domain, - query: DocumentQuery, - options?: Pick, 'sort' | 'limit' | 'projection'> - ): Promise> { - let cursor = this.db.collection(domain).find(this.translateRawQuery(query)) - if (options?.limit !== undefined) { - cursor = cursor.limit(options.limit) - } - if (options !== null && options !== undefined) { - if (options.sort !== undefined) { - const sort: Sort = {} - for (const key in options.sort) { - const order = options.sort[key] === SortingOrder.Ascending ? 1 : -1 - ;(sort as any)[key] = order - } - cursor = cursor.sort(sort) - } - } - return { - next: async (size: number) => { - const docs: T[] = [] - while (docs.length < size && (await cursor.hasNext())) { - try { - const d = await cursor.next() - if (d !== null) { - docs.push(d) - } else { - break - } - } catch (err) { - console.error(err) - return null - } - } - return docs - }, - close: () => cursor.close() - } - } - - private translateRawQuery(query: DocumentQuery): Filter { - const translated: any = {} - for (const key in query) { - const value = (query as any)[key] - if (value !== null && typeof value === 'object') { - const keys = Object.keys(value) - if (keys[0] === '$like') { - const pattern = value.$like as string - translated[key] = { - $regex: `^${pattern.split('%').join('.*')}$`, - $options: 'i' - } - continue - } - } - translated[key] = value - } - return translated - } - - async rawFindAll(domain: Domain, query: DocumentQuery, options?: FindOptions): Promise { - let cursor = this.db.collection(domain).find(this.translateRawQuery(query)) - if (options?.limit !== undefined) { - cursor = cursor.limit(options.limit) - } - if (options !== null && options !== undefined) { - if (options.sort !== undefined) { - const sort: Sort = {} - for (const key in options.sort) { - const order = options.sort[key] === SortingOrder.Ascending ? 1 : -1 - ;(sort as any)[key] = order - } - cursor = cursor.sort(sort) - } - } - if (options?.projection !== undefined) { - const projection: Projection = {} - for (const key in options.projection ?? []) { - projection[key] = options.projection[key] - } - cursor = cursor.project(projection) - } - return await cursor.toArray() - } - - async rawUpdate( - domain: Domain, - query: DocumentQuery, - operations: DocumentUpdate - ): Promise { - if (isOperator(operations)) { - await this.db.collection(domain).updateMany(this.translateRawQuery(query), { $set: { '%hash%': this.curHash() } }) - await this.db - .collection(domain) - .updateMany(this.translateRawQuery(query), { ...operations } as unknown as UpdateFilter) - } else { - await this.db - .collection(domain) - .updateMany(this.translateRawQuery(query), { $set: { ...operations, '%hash%': this.curHash() } }) - } - } - - rawDeleteMany(domain: Domain, query: DocumentQuery): Promise { - return this.db.collection(domain).deleteMany(this.translateRawQuery(query)).then() - } - - abstract init (ctx: MeasureContext): Promise - - collection(domain: Domain): Collection { - return this._db.collection(domain) - } - - helper (): DomainHelperOperations { - return this._db - } - - async tx (ctx: MeasureContext, ...tx: Tx[]): Promise { - return [] - } - - close (): Promise { - this.client.close() - return Promise.resolve() - } - - private translateQuery( - clazz: Ref>, - query: DocumentQuery, - options?: ServerFindOptions - ): { base: Filter, lookup: Filter } { - const translatedBase: any = {} - const translatedLookup: any = {} - - const mixins = new Set>>() - for (const key in query) { - const value = (query as any)[key] - - const tkey = this.translateKey(key, clazz, mixins) - - const translated = tkey.lookup ? translatedLookup : translatedBase - if (value !== null && typeof value === 'object') { - const keys = Object.keys(value) - if (keys[0] === '$like') { - translated[tkey.key] = translateLikeQuery(value.$like as string) - continue - } - } - translated[tkey.key] = value - } - if (options?.skipSpace === true) { - delete translatedBase.space - } - if (options?.skipClass === true) { - delete translatedBase._class - return { base: translatedBase, lookup: translatedLookup } - } - const baseClass = this.hierarchy.getBaseClass(clazz) - if (baseClass !== core.class.Doc) { - const classes = Array.from( - new Set(this.hierarchy.getDescendants(baseClass).filter((it) => !this.hierarchy.isMixin(it))) - ) - - // Only replace if not specified - if (translatedBase._class === undefined) { - translatedBase._class = { $in: classes } - } else if (typeof translatedBase._class === 'string') { - if (!classes.includes(translatedBase._class)) { - translatedBase._class = classes.length === 1 ? classes[0] : { $in: classes } - } - } else if (typeof translatedBase._class === 'object' && translatedBase._class !== null) { - let descendants: Ref>[] = classes - - if (Array.isArray(translatedBase._class.$in)) { - const classesIds = new Set(classes) - descendants = translatedBase._class.$in.filter((c: Ref>) => classesIds.has(c)) - } - - if (translatedBase._class != null && Array.isArray(translatedBase._class.$nin)) { - const excludedClassesIds = new Set>>(translatedBase._class.$nin) - descendants = descendants.filter((c) => !excludedClassesIds.has(c)) - } - - const desc = Array.from( - new Set(descendants.filter((it: any) => !this.hierarchy.isMixin(it as Ref>))) - ) - translatedBase._class = desc.length === 1 ? desc[0] : { $in: desc } - } - - if (baseClass !== clazz && !mixins.has(clazz)) { - // Add an mixin to be exists flag - translatedBase[clazz] = { $exists: true } - } - } else { - // No need to pass _class in case of fixed domain search. - if ('_class' in translatedBase) { - delete translatedBase._class - } - } - if (translatedBase._class?.$in != null && Array.isArray(translatedBase._class.$in)) { - translatedBase._class.$in = Array.from(new Set(translatedBase._class.$in)) - } - if (translatedBase._class?.$in?.length === 1 && translatedBase._class?.$nin === undefined) { - translatedBase._class = translatedBase._class.$in[0] - } - return { base: translatedBase, lookup: translatedLookup } - } - - private getLookupValue( - clazz: Ref>, - lookup: Lookup, - result: LookupStep[], - parent?: string - ): void { - for (const key in lookup) { - if (key === '_id') { - this.getReverseLookupValue(lookup, result, parent) - continue - } - const value = (lookup as any)[key] - if (Array.isArray(value)) { - const [_class, nested] = value - const tkey = this.checkMixinKey(key, clazz) - const fullKey = parent !== undefined ? parent + '.' + tkey : tkey - const domain = this.hierarchy.getDomain(_class) - if (domain !== DOMAIN_MODEL) { - result.push({ - from: domain, - localField: fullKey, - foreignField: '_id', - as: fullKey.split('.').join('') + '_lookup' - }) - } - this.getLookupValue(_class, nested, result, fullKey + '_lookup') - } else { - const _class = value as Ref> - const tkey = this.checkMixinKey(key, clazz) - const fullKey = parent !== undefined ? parent + '.' + tkey : tkey - const domain = this.hierarchy.getDomain(_class) - if (domain !== DOMAIN_MODEL) { - result.push({ - from: domain, - localField: fullKey, - foreignField: '_id', - as: fullKey.split('.').join('') + '_lookup' - }) - } - } - } - } - - private getReverseLookupValue (lookup: ReverseLookups, result: LookupStep[], parent?: string): void { - const fullKey = parent !== undefined ? parent + '.' + '_id' : '_id' - const lid = lookup?._id ?? {} - for (const key in lid) { - const as = parent !== undefined ? parent + key : key - const value = lid[key] - - let _class: Ref> - let attr = 'attachedTo' - - if (Array.isArray(value)) { - _class = value[0] - attr = value[1] - } else { - _class = value - } - const domain = this.hierarchy.getDomain(_class) - const desc = this.hierarchy - .getDescendants(this.hierarchy.getBaseClass(_class)) - .filter((it) => !this.hierarchy.isMixin(it)) - if (domain !== DOMAIN_MODEL) { - const asVal = as.split('.').join('') + '_lookup' - const step: LookupStep = { - from: domain, - localField: fullKey, - foreignField: attr, - pipeline: [ - { - $match: { - _class: desc.length === 1 ? desc[0] : { $in: desc } - } - } - ], - as: asVal - } - result.push(step) - } - } - } - - private getLookups( - _class: Ref>, - lookup: Lookup | undefined, - parent?: string - ): LookupStep[] { - if (lookup === undefined) return [] - const result: [] = [] - this.getLookupValue(_class, lookup, result, parent) - return result - } - - private getAssociations (associations: AssociationQuery[]): LookupStep[] { - const res: LookupStep[] = [] - for (const association of associations) { - const assoc = this.modelDb.findObject(association[0]) - if (assoc === undefined) continue - const isReverse = association[1] === -1 - const _class = !isReverse ? assoc.classB : assoc.classA - const targetDomain = this.hierarchy.getDomain(_class) - if (targetDomain === DOMAIN_MODEL) continue - const as = association[0] + '_hidden_association' - res.push({ - from: DOMAIN_RELATION, - localField: '_id', - foreignField: isReverse ? 'docB' : 'docA', - as - }) - res.push({ - from: targetDomain, - localField: as + '.' + (isReverse ? 'docA' : 'docB'), - foreignField: '_id', - as: association[0] + '_association' - }) - } - return res - } - - private fillLookup( - _class: Ref>, - object: any, - key: string, - fullKey: string, - targetObject: any - ): void { - if (targetObject.$lookup === undefined) { - targetObject.$lookup = {} - } - const domain = this.hierarchy.getDomain(_class) - if (domain !== DOMAIN_MODEL) { - const arr = object[fullKey] - if (arr !== undefined && Array.isArray(arr)) { - if (arr.length === 1) { - targetObject.$lookup[key] = arr[0] - } else if (arr.length > 1) { - targetObject.$lookup[key] = arr - } - } - } else { - targetObject.$lookup[key] = this.modelDb.findAllSync(_class, { _id: targetObject[key] })[0] - } - } - - private fillAssociationsValue (associations: AssociationQuery[], object: any): Record { - const res: Record = {} - for (const association of associations) { - const assocKey = association[0] + '_hidden_association' - const data = object[assocKey] - if (data !== undefined && Array.isArray(data)) { - const filtered = new Set( - data.filter((it) => it.association === association[0]).map((it) => (association[1] === 1 ? it.docB : it.docA)) - ) - const fullKey = association[0] + '_association' - const arr = object[fullKey] - if (arr !== undefined && Array.isArray(arr)) { - res[association[0]] = arr.filter((it) => filtered.has(it._id)) - } - } - } - return res - } - - private fillLookupValue( - ctx: MeasureContext, - clazz: Ref>, - lookup: Lookup | undefined, - object: any, - parent?: string, - parentObject?: any - ): void { - if (lookup === undefined) return - for (const key in lookup) { - if (key === '_id') { - this.fillReverseLookup(clazz, lookup, object, parent, parentObject) - continue - } - const value = (lookup as any)[key] - const tkey = this.checkMixinKey(key, clazz).split('.').join('') - const fullKey = parent !== undefined ? parent + tkey + '_lookup' : tkey + '_lookup' - const targetObject = parentObject ?? object - if (Array.isArray(value)) { - const [_class, nested] = value - this.fillLookup(_class, object, key, fullKey, targetObject) - this.fillLookupValue(ctx, _class, nested, object, fullKey, targetObject.$lookup[key]) - } else { - this.fillLookup(value, object, key, fullKey, targetObject) - } - } - } - - private fillReverseLookup( - clazz: Ref>, - lookup: ReverseLookups, - object: any, - parent?: string, - parentObject?: any - ): void { - const targetObject = parentObject ?? object - if (targetObject.$lookup === undefined) { - targetObject.$lookup = {} - } - for (const key in lookup._id) { - const value = lookup._id[key] - let _class: Ref> - let attr = 'attachedTo' - - if (Array.isArray(value)) { - _class = value[0] - attr = value[1] - } else { - _class = value - } - const domain = this.hierarchy.getDomain(_class) - const tkey = this.checkMixinKey(key, clazz).split('.').join('') - const fullKey = parent !== undefined ? parent + tkey + '_lookup' : tkey + '_lookup' - if (domain !== DOMAIN_MODEL) { - const arr = object[fullKey] - targetObject.$lookup[key] = arr - } else { - const arr = this.modelDb.findAllSync(_class, { [attr]: targetObject._id }) - targetObject.$lookup[key] = arr - } - } - } - - private fillSortPipeline( - clazz: Ref>, - options: FindOptions | undefined, - pipeline: any[] - ): void { - if (options?.sort !== undefined) { - const sort = {} as any - for (const _key in options.sort) { - const { key } = this.translateKey(_key, clazz) - - if (typeof options.sort[_key] === 'object') { - const rules = options.sort[_key] as SortingRules - fillCustomSort(rules, key, pipeline, sort, options, _key) - } else if (this.isDate(clazz, _key)) { - fillDateSort(key, pipeline, sort, options, _key) - } else { - // Sort enum if no special sorting is defined. - const enumOf = this.getEnumById(clazz, _key) - if (enumOf !== undefined) { - fillEnumSort(enumOf, key, pipeline, sort, options, _key) - } else { - // Ordinary sort field. - sort[key] = options.sort[_key] === SortingOrder.Ascending ? 1 : -1 - } - } - } - pipeline.push({ $sort: sort }) - } - } - - private async findWithPipeline( - ctx: MeasureContext, - domain: Domain, - clazz: Ref>, - query: DocumentQuery, - options: ServerFindOptions, - stTime: number - ): Promise> { - const st = platformNow() - const pipeline: any[] = [] - const tquery = this.translateQuery(clazz, query, options) - - const slowPipeline = isLookupQuery(query) || isLookupSort(options?.sort) - const steps = this.getLookups(clazz, options?.lookup) - - if (options.associations !== undefined && options.associations.length > 0) { - const assoc = this.getAssociations(options.associations) - steps.push(...assoc) - } - - if (slowPipeline) { - if (Object.keys(tquery.base).length > 0) { - pipeline.push({ $match: tquery.base }) - } - for (const step of steps) { - pipeline.push({ $lookup: step }) - } - if (Object.keys(tquery.lookup).length > 0) { - pipeline.push({ $match: tquery.lookup }) - } - } else { - if (Object.keys(tquery.base).length > 0) { - pipeline.push({ $match: { ...tquery.base, ...tquery.lookup } }) - } - } - const totalPipeline: any[] = [...pipeline] - this.fillSortPipeline(clazz, options, pipeline) - if (options?.limit !== undefined || typeof query._id === 'string') { - pipeline.push({ $limit: options?.limit ?? 1 }) - } - if (!slowPipeline) { - for (const step of steps) { - pipeline.push({ $lookup: step }) - } - } - if (options?.projection !== undefined) { - const projection: Projection = {} - for (const key in options.projection) { - const ckey = this.checkMixinKey(key, clazz) as keyof T - projection[ckey] = options.projection[key] - } - for (const step of steps) { - // We also need to add lookup if original field are in projection. - if ((projection as any)[step.from] === 1) { - ;(projection as any)[step.as] = 1 - } - } - pipeline.push({ $project: projection }) - } - - const cursor = this.collection(domain).aggregate>(pipeline) - let result: WithLookup[] = [] - let total = options?.total === true ? 0 : -1 - try { - result = await ctx.with( - 'aggregate', - {}, - (ctx) => toArray(cursor), - () => ({ - domain, - pipeline, - clazz - }) - ) - } catch (e) { - console.error('error during executing cursor in findWithPipeline', clazz, cutObjectArray(query), options, e) - throw e - } - for (const row of result) { - ctx.withSync('fill-lookup', {}, (ctx) => { - this.fillLookupValue(ctx, clazz, options?.lookup, row) - }) - if (row.$lookup !== undefined) { - for (const [, v] of Object.entries(row.$lookup)) { - this.stripHash(v) - } - } - if (options.associations !== undefined && options.associations.length > 0) { - row.$associations = this.fillAssociationsValue(options.associations, row) - for (const [, v] of Object.entries(row.$associations)) { - this.stripHash(v) - } - } - this.clearExtraLookups(row) - } - if (options?.total === true) { - totalPipeline.push({ $count: 'total' }) - const totalCursor = this.collection(domain).aggregate(totalPipeline, { - checkKeys: false - }) - const arr = await ctx.with( - 'aggregate-total', - {}, - (ctx) => toArray(totalCursor), - () => ({ - domain, - pipeline, - clazz - }) - ) - total = arr?.[0]?.total ?? 0 - } - const edTime = platformNow() - if (edTime - stTime > 1000 || st - stTime > 1000) { - ctx.error('aggregate', { - time: edTime - stTime, - clazz, - query: cutObjectArray(query), - options, - queueTime: st - stTime - }) - } - this.handleEvent(domain, 'read', result.length) - return toFindResult(this.stripHash(result) as T[], total) - } - - private translateKey( - key: string, - clazz: Ref>, - mixins?: Set>> - ): { key: string, lookup: boolean } { - const arr = key.split('.').filter((p) => p) - let tKey = '' - let lookup = false - - for (let i = 0; i < arr.length; i++) { - const element = arr[i] - if (element === '$lookup') { - tKey += arr[++i] + '_lookup' - lookup = true - } else { - if (!tKey.endsWith('.') && i > 0) { - tKey += '.' - } - tKey += arr[i] - if (i !== arr.length - 1) { - tKey += '.' - } - } - // Check if key is belong to mixin class, we need to add prefix. - tKey = this.checkMixinKey(tKey, clazz, mixins) - } - - return { key: tKey, lookup } - } - - private clearExtraLookups (row: any): void { - for (const key in row) { - if (key.endsWith('_lookup') || key.endsWith('_association')) { - // eslint-disable-next-line - delete row[key] - } - } - } - - private checkMixinKey(key: string, clazz: Ref>, mixins?: Set>>): string { - if (!key.includes('.')) { - try { - const attr = this.hierarchy.findAttribute(clazz, key) - if (attr !== undefined && this.hierarchy.isMixin(attr.attributeOf)) { - // It is mixin - key = attr.attributeOf + '.' + key - mixins?.add(attr.attributeOf) - } - } catch (err: any) { - // ignore, if - } - } - return key - } - - private getEnumById(_class: Ref>, key: string): Enum | undefined { - const attr = this.hierarchy.findAttribute(_class, key) - if (attr !== undefined) { - if (attr.type._class === core.class.EnumOf) { - const ref = (attr.type as EnumOf).of - return this.modelDb.getObject(ref) - } - } - return undefined - } - - private isEnumSort(_class: Ref>, options?: FindOptions): boolean { - if (options?.sort === undefined) return false - return Object.keys(options.sort).some( - (key) => this.hierarchy.findAttribute(_class, key)?.type?._class === core.class.EnumOf - ) - } - - private isDate(_class: Ref>, key: string): boolean { - const attr = this.hierarchy.findAttribute(_class, key) - if (attr !== undefined) { - return attr.type._class === core.class.TypeDate - } - return false - } - - private isRulesSort(options?: FindOptions): boolean { - if (options?.sort !== undefined) { - return Object.values(options.sort).some((it) => typeof it === 'object') - } - return false - } - - @withContext('groupBy') - groupBy( - ctx: MeasureContext, - domain: Domain, - field: string, - query?: DocumentQuery - ): Promise> { - return ctx.with('groupBy', { domain }, async (ctx) => { - const coll = this.collection(domain) - const grResult = await coll - .aggregate([ - ...(query !== undefined ? [{ $match: query }] : []), - { - $group: { - _id: '$' + field, - count: { $sum: 1 } - } - } - ]) - .toArray() - return new Map(grResult.map((it) => [it._id as unknown as T, it.count])) - }) - } - - findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: ServerFindOptions - ): Promise> { - const stTime = platformNow() - const mongoQuery = this.translateQuery(_class, query, options) - const fQuery = { ...mongoQuery.base, ...mongoQuery.lookup } - return addOperation(ctx, 'find-all', {}, async () => { - const st = platformNow() - let result: FindResult - const domain = this.hierarchy.getDomain(_class) - if ( - options?.lookup != null || - options?.associations != null || - this.isEnumSort(_class, options) || - this.isRulesSort(options) - ) { - return await this.findWithPipeline(ctx, domain, _class, query, options ?? {}, stTime) - } - const coll = this.collection(domain) - - if (options?.limit === 1 || typeof query._id === 'string') { - // Skip sort/projection/etc. - return await ctx.with( - 'find-one', - {}, - async (ctx) => { - const findOptions: MongoFindOptions = {} - - if (options?.sort !== undefined) { - findOptions.sort = this.collectSort(options, _class) - } - if (options?.projection !== undefined) { - findOptions.projection = this.calcProjection(options, _class) - } - - let doc: WithId | null - - if (typeof fQuery._id === 'string') { - doc = await coll.findOne({ _id: fQuery._id }, findOptions) - if (doc != null && matchQuery([doc as unknown as Doc], query, _class, this.hierarchy).length === 0) { - doc = null - } - } else { - doc = await coll.findOne(fQuery, findOptions) - } - - let total = -1 - if (options?.total === true) { - total = await coll.countDocuments({ ...mongoQuery.base, ...mongoQuery.lookup }) - } - if (doc != null) { - return toFindResult([this.stripHash(doc as unknown as T) as T], total) - } - return toFindResult([], total) - }, - { domain, mongoQuery, _idOnly: typeof fQuery._id === 'string' } - ) - } - - let cursor = coll.find(fQuery) - - if (options?.projection !== undefined) { - const projection = this.calcProjection(options, _class) - if (projection != null) { - cursor = cursor.project(projection) - } - } - let total: number = -1 - if (options != null) { - if (options.sort !== undefined) { - const sort = this.collectSort(options, _class) - if (sort !== undefined) { - cursor = cursor.sort(sort) - } - } - if (options.limit !== undefined || typeof query._id === 'string') { - if (options.total === true) { - total = await coll.countDocuments(fQuery) - } - cursor = cursor.limit(options.limit ?? 1) - } - } - // Error in case of timeout - try { - const res: T[] = await ctx.with( - 'find-all', - {}, - (ctx) => toArray(cursor), - () => ({ - queueTime: stTime - st, - mongoQuery, - options, - domain - }) - ) - if (options?.total === true && options?.limit === undefined) { - total = res.length - } - result = toFindResult(this.stripHash(res) as T[], total) - } catch (e) { - console.error('error during executing cursor in findAll', _class, cutObjectArray(query), options, e) - throw e - } - - const edTime = platformNow() - if (edTime - st > 1000 || st - stTime > 1000) { - ctx.error('FindAll', { - time: edTime - st, - _class, - query: fQuery, - options, - queueTime: st - stTime - }) - } - this.handleEvent(domain, 'read', result.length) - return result - }) - } - - private collectSort( - options: - | (FindOptions & { - domain?: Domain | undefined // Allow to find for Doc's in specified domain only. - }) - | undefined, - _class: Ref> - ): Sort | undefined { - if (options?.sort === undefined) { - return undefined - } - const sort: Sort = {} - let count = 0 - for (const key in options.sort) { - const ckey = this.checkMixinKey(key, _class) - const order = options.sort[key] === SortingOrder.Ascending ? 1 : -1 - ;(sort as any)[ckey] = order - count++ - } - if (count === 0) { - return undefined - } - return sort - } - - private calcProjection( - options: - | (FindOptions & { - domain?: Domain | undefined // Allow to find for Doc's in specified domain only. - }) - | undefined, - _class: Ref> - ): Projection | undefined { - if (options?.projection === undefined) { - return undefined - } - const projection: Projection = {} - let count = 0 - for (const key in options.projection ?? []) { - const ckey = this.checkMixinKey(key, _class) as keyof T - projection[ckey] = options.projection[key] - count++ - } - if (options.sort != null) { - for (const k of Object.keys(options.sort) as (keyof T)[]) { - if (projection[k] == null) { - ;(projection as any)[k] = 1 - } - } - } - if (count === 0) { - return undefined - } - return projection - } - - stripHash(docs: T | T[]): T | T[] { - if (Array.isArray(docs)) { - docs.forEach((it) => { - if ('%hash%' in it) { - delete it['%hash%'] - } - return it - }) - } else if (typeof docs === 'object' && docs != null) { - if ('%hash%' in docs) { - delete docs['%hash%'] - } - } - return docs - } - - curHash (): string { - return Date.now().toString(16) // Current hash value - } - - @withContext('get-domain-hash') - async getDomainHash (ctx: MeasureContext, domain: Domain): Promise { - return await calcHashHash(ctx, domain, this) - } - - strimSize (str?: string): string { - if (str == null) { - return '' - } - const pos = str.indexOf('|') - if (pos > 0) { - return str.substring(0, pos) - } - return str - } - - find (_ctx: MeasureContext, domain: Domain): StorageIterator { - const ctx = _ctx.newChild('find', { domain }) - const coll = this.db.collection(domain) - let iterator: FindCursor - - return { - next: async () => { - if (iterator === undefined) { - iterator = coll.find( - {}, - { - projection: { - '%hash%': 1, - _id: 1 - } - } - ) - } - const d = await ctx.with('next', {}, () => iterator.next()) - const result: DocInfo[] = [] - if (d != null) { - result.push({ - id: (d._id as any) instanceof ObjectId ? d._id.toString() : d._id, - hash: this.strimSize((d as any)['%hash%']) - }) - } - if (iterator.bufferedCount() > 0) { - result.push( - ...iterator.readBufferedDocuments().map((it) => ({ - id: (it._id as any) instanceof ObjectId ? it._id.toString() : it._id, - hash: this.strimSize((it as any)['%hash%']) - })) - ) - } - return result - }, - close: async () => { - await ctx.with('close', {}, () => iterator.close()) - ctx.end() - } - } - } - - rawFind (_ctx: MeasureContext, domain: Domain): RawFindIterator { - const ctx = _ctx.newChild('findRaw', { domain }) - const coll = this.db.collection(domain) - let iterator: FindCursor - - return { - find: async () => { - if (iterator === undefined) { - iterator = coll.find({}) - } - const d = await ctx.with('next', {}, () => iterator.next()) - const result: Doc[] = [] - if (d != null) { - result.push(this.stripHash(d) as Doc) - } - if (iterator.bufferedCount() > 0) { - result.push(...(this.stripHash(iterator.readBufferedDocuments()) as Doc[])) - } - return result ?? [] - }, - close: async () => { - await ctx.with('close', {}, () => iterator.close()) - ctx.end() - } - } - } - - load (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return ctx.with('load', { domain }, async () => { - if (docs.length === 0) { - return [] - } - const cursor = this.db.collection(domain).find({ _id: { $in: docs } }, { limit: docs.length }) - const result = await toArray(cursor) - return this.stripHash(result) as Doc[] - }) - } - - upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise { - return ctx.with('upload', { domain }, (ctx) => { - const coll = this.collection(domain) - - return uploadDocuments(ctx, docs, coll, this.curHash()) - }) - } - - clean (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return ctx.with('clean', {}, async () => { - if (docs.length > 0) { - await this.db.collection(domain).deleteMany({ _id: { $in: docs } }) - } - }) - } -} - -interface OperationBulk { - bulks: number - add: Doc[] - update: Map, Partial> - - bulkOperations: AnyBulkWriteOperation[] - - findUpdate: Set> - - raw: (() => Promise)[] -} - -class MongoAdapter extends MongoAdapterBase { - async init (ctx: MeasureContext): Promise { - await this._db.init() - } - - updateBulk (bulk: OperationBulk, tx: Tx): void { - switch (tx._class) { - case core.class.TxCreateDoc: - this.txCreateDoc(bulk, tx as TxCreateDoc) - break - case core.class.TxUpdateDoc: - this.txUpdateDoc(bulk, tx as TxUpdateDoc) - break - case core.class.TxRemoveDoc: - this.txRemoveDoc(bulk, tx as TxRemoveDoc) - break - case core.class.TxMixin: - this.txMixin(bulk, tx as TxMixin) - break - case core.class.TxApplyIf: - return undefined - default: - console.error('Unknown/Unsupported operation:', tx._class, tx) - break - } - } - - async tx (ctx: MeasureContext, ...txes: Tx[]): Promise { - const result: TxResult[] = [] - - const h = this.hierarchy - const byDomain = groupByArray(txes, (it) => { - if (TxProcessor.isExtendsCUD(it._class)) { - return h.findDomain((it as TxCUD).objectClass) - } - return undefined - }) - - const stTime = platformNow() - const st = stTime - let promises: Promise[] = [] - for (const [domain, txs] of byDomain) { - if (domain === undefined) { - continue - } - const domainBulk: OperationBulk = { - bulks: 1, - add: [], - update: new Map(), - bulkOperations: [], - findUpdate: new Set(), - raw: [] - } - for (const t of txs) { - this.updateBulk(domainBulk, t) - } - if ( - domainBulk.add.length === 0 && - domainBulk.update.size === 0 && - domainBulk.bulkOperations.length === 0 && - domainBulk.findUpdate.size === 0 && - domainBulk.raw.length === 0 - ) { - continue - } - - // Minir optimizations - // Add Remove optimization - - const ops: AnyBulkWriteOperation[] = [] - - if (domainBulk.add.length > 0) { - ops.push(...domainBulk.add.map((it) => ({ insertOne: { document: it } }))) - this.handleEvent(domain, 'add', domainBulk.add.length) - } - if (domainBulk.update.size > 0) { - // Extract similar update to update many if possible - // TODO: - - ops.push( - ...Array.from(domainBulk.update.entries()).map((it) => ({ - updateOne: { - filter: { _id: it[0] }, - update: { - $set: it[1] - } - } - })) - ) - this.handleEvent(domain, 'update', domainBulk.update.size) - } - if (domainBulk.bulkOperations.length > 0) { - ops.push(...domainBulk.bulkOperations) - this.handleEvent(domain, 'update', domainBulk.bulkOperations.length) - } - - if (ops.length > 0) { - if (ops === undefined || ops.length === 0) { - continue - } - const coll = this.db.collection(domain) - - promises.push( - addOperation(ctx, 'bulk-write', { domain, operations: ops.length }, (ctx) => - ctx.with( - 'bulk-write', - { domain }, - async () => { - try { - await coll.bulkWrite(ops, { - ordered: false - }) - } catch (err: any) { - ctx.error('failed to perform bulk write', { error: err, txes: cutObjectArray(ops) }) - } - }, - { - domain, - operations: ops.length - } - ) - ) - ) - } - if (domainBulk.findUpdate.size > 0) { - if (promises.length > 0) { - await Promise.all(promises) - promises = [] - } - const coll = this.db.collection(domain) - - await ctx.with( - 'find-result', - {}, - async (ctx) => { - const st = platformNow() - const docs = await addOperation( - ctx, - 'find-result', - {}, - (ctx) => coll.find({ _id: { $in: Array.from(domainBulk.findUpdate) } }).toArray(), - { domain, _ids: domainBulk.findUpdate.size, queueTime: stTime - st } - ) - result.push(...docs) - this.handleEvent(domain, 'read', docs.length) - }, - { - domain, - queueTime: stTime - st - } - ) - } - - if (domainBulk.raw.length > 0) { - if (promises.length > 0) { - await Promise.all(promises) - promises = [] - } - await ctx.with( - 'raw', - {}, - async (ctx) => { - for (const r of domainBulk.raw) { - result.push({ object: await addOperation(ctx, 'raw-op', {}, () => r()) }) - } - }, - { - domain, - queueTime: stTime - st - } - ) - } - } - if (promises.length > 0) { - await Promise.all(promises) - } - return result - } - - protected txRemoveDoc (bulk: OperationBulk, tx: TxRemoveDoc): void { - bulk.bulkOperations.push({ deleteOne: { filter: { _id: tx.objectId } } }) - } - - protected txMixin (bulk: OperationBulk, tx: TxMixin): void { - const filter = { _id: tx.objectId } - const modifyOp = { - modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn, - '%hash%': this.curHash() - } - if (isOperator(tx.attributes)) { - const update = { ...this.translateMixinAttrs(tx.mixin, tx.attributes), $set: { ...modifyOp } } - - bulk.bulkOperations.push({ - updateOne: { - filter, - update - } - }) - return - } - const update = { ...this.translateMixinAttrs(tx.mixin, tx.attributes), ...modifyOp } - - let upd = bulk.update.get(tx.objectId) - if (upd === undefined) { - upd = {} - bulk.update.set(tx.objectId, upd) - } - - for (const [k, v] of Object.entries(update)) { - ;(upd as any)[k] = v - } - } - - private translateMixinAttrs (mixin: Ref>, attributes: Record): Record { - const attrs: Record = {} - let count = 0 - for (const [k, v] of Object.entries(attributes)) { - if (k.startsWith('$')) { - attrs[k] = this.translateMixinAttrs(mixin, v) - } else { - attrs[mixin + '.' + k] = v - } - count++ - } - - if (count === 0) { - // We need at least one attribute, to be inside for first time, - // for mongo to create embedded object, if we don't want to get object first. - attrs[mixin + '.' + '__mixin'] = 'true' - } - return attrs - } - - protected txCreateDoc (bulk: OperationBulk, tx: TxCreateDoc): void { - const doc = TxProcessor.createDoc2Doc(tx) - bulk.add.push(translateDoc(doc, this.curHash())) - } - - protected txUpdateDoc (bulk: OperationBulk, tx: TxUpdateDoc): void { - if (isOperator(tx.operations)) { - const operator = Object.keys(tx.operations)[0] - if (operator === '$update') { - const keyval = (tx.operations as any).$update - const arr = Object.keys(keyval)[0] - const desc = keyval[arr] as QueryUpdate - const ops = [ - { - updateOne: { - filter: { - _id: tx.objectId, - ...Object.fromEntries(Object.entries(desc.$query).map((it) => [arr + '.' + it[0], it[1]])) - }, - update: { - $set: { - ...Object.fromEntries(Object.entries(desc.$update).map((it) => [arr + '.$.' + it[0], it[1]])), - '%hash%': this.curHash() - } - } - } - }, - { - updateOne: { - filter: { _id: tx.objectId }, - update: { - $set: { - modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn, - '%hash%': this.curHash() - } - } - } - } - ] - bulk.bulkOperations.push(...ops) - } else { - const domain = this.hierarchy.getDomain(tx.objectClass) - - if (tx.retrieve === true) { - bulk.raw.push(async () => { - const res = await this.collection(domain).findOneAndUpdate( - { _id: tx.objectId }, - { - ...tx.operations, - $set: { - modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn, - '%hash%': this.curHash() - } - } as unknown as UpdateFilter, - { returnDocument: 'after', includeResultMetadata: true } - ) - this.handleEvent(domain, 'read', 1) - this.handleEvent(domain, 'update', 1) - return res.value as TxResult - }) - } else { - bulk.bulkOperations.push({ - updateOne: { - filter: { _id: tx.objectId }, - update: { - ...tx.operations, - $set: { - modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn, - '%hash%': this.curHash() - } - } - } - }) - } - } - } else { - let upd = bulk.update.get(tx.objectId) - if (upd === undefined) { - upd = {} - bulk.update.set(tx.objectId, upd) - } - - for (const [k, v] of Object.entries({ - ...tx.operations, - modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn, - '%hash%': this.curHash() - })) { - ;(upd as any)[k] = v - } - - if (tx.retrieve === true) { - bulk.findUpdate.add(tx.objectId) - } - } - } -} - -class MongoTxAdapter extends MongoAdapterBase implements TxAdapter { - txColl: Collection | undefined - - async init (ctx: MeasureContext): Promise { - await this._db.init(DOMAIN_TX) - await this._db.init(DOMAIN_MODEL_TX) - } - - override async tx (ctx: MeasureContext, ...tx: Tx[]): Promise { - if (tx.length === 0) { - return [] - } - const opName = tx.length === 1 ? 'tx-one' : 'tx' - const modelTxes: Tx[] = [] - const baseTxes: Tx[] = [] - for (const _tx of tx) { - if (_tx.objectSpace === core.space.Model) { - modelTxes.push(_tx) - } else { - baseTxes.push(_tx) - } - } - if (baseTxes.length > 0) { - await addOperation( - ctx, - opName, - {}, - async (ctx) => { - await ctx.with( - 'insertMany', - { domain: 'tx' }, - async () => { - try { - const hash = this.curHash() - await this.txCollection().insertMany( - baseTxes.map((it) => translateDoc(it, hash)), - { - ordered: false - } - ) - } catch (err: any) { - ctx.error('failed to write tx', { error: err, message: err.message }) - } - }, - - { - count: baseTxes.length - } - ) - }, - { domain: 'tx', count: baseTxes.length } - ) - } - if (modelTxes.length > 0) { - await addOperation( - ctx, - opName, - {}, - async (ctx) => { - await ctx.with( - 'insertMany', - { domain: DOMAIN_MODEL_TX }, - async () => { - try { - const hash = this.curHash() - await this.db.collection(DOMAIN_MODEL_TX).insertMany( - modelTxes.map((it) => translateDoc(it, hash)), - { - ordered: false - } - ) - } catch (err: any) { - ctx.error('failed to write model tx', { error: err, message: err.message }) - } - }, - { - count: modelTxes.length - } - ) - }, - { domain: DOMAIN_MODEL_TX, count: modelTxes.length } - ) - } - ctx.withSync('handleEvent', {}, () => { - this.handleEvent(DOMAIN_TX, 'add', tx.length) - }) - return [] - } - - private txCollection (): Collection { - if (this.txColl !== undefined) { - return this.txColl - } - this.txColl = this.db.collection(DOMAIN_TX) - return this.txColl - } - - @withContext('get-model') - async getModel (ctx: MeasureContext): Promise { - const txCollection = this.db.collection(DOMAIN_MODEL_TX) - const cursor = txCollection.find({}, { sort: { modifiedOn: 1, _id: 1 } }) - const model = await toArray(cursor) - // We need to put all core.account.System transactions first - const systemTx: Tx[] = [] - const userTx: Tx[] = [] - - // Ignore old Employee accounts. - function isPersonAccount (tx: Tx): boolean { - return ( - (tx._class === core.class.TxCreateDoc || - tx._class === core.class.TxUpdateDoc || - tx._class === core.class.TxRemoveDoc) && - (tx as TxCUD).objectClass === 'contact:class:PersonAccount' - ) - } - model.forEach((tx) => (tx.modifiedBy === core.account.System && !isPersonAccount(tx) ? systemTx : userTx).push(tx)) - return this.stripHash(systemTx.concat(userTx)) as Tx[] - } -} - -export async function uploadDocuments ( - ctx: MeasureContext, - docs: Doc[], - coll: Collection, - curHash: string -): Promise { - const ops = Array.from(docs) - - while (ops.length > 0) { - const part = ops.splice(0, 500) - await coll.bulkWrite( - part.map((it) => { - const digest: string = (it as any)['%hash%'] ?? curHash - return { - replaceOne: { - filter: { _id: it._id }, - replacement: { ...it, '%hash%': digest }, - upsert: true - } - } - }), - { - ordered: false - } - ) - } -} - -function fillEnumSort ( - enumOf: Enum, - key: string, - pipeline: any[], - sort: any, - options: FindOptions, - _key: string -): void { - const branches = enumOf.enumValues.map((value, index) => { - return { case: { $eq: [`$${key}`, value] }, then: index } - }) - pipeline.push({ - $addFields: { - [`sort_${key}`]: { - $switch: { - branches, - default: enumOf.enumValues.length - } - } - } - }) - if (options.sort === undefined) { - options.sort = {} - } - sort[`sort_${key}`] = options.sort[_key] === SortingOrder.Ascending ? 1 : -1 -} -function fillDateSort (key: string, pipeline: any[], sort: any, options: FindOptions, _key: string): void { - if (options.sort === undefined) { - options.sort = {} - } - pipeline.push({ - $addFields: { - [`sort_isNull_${key}`]: { $or: [{ $eq: [`$${key}`, null] }, { $eq: [{ $type: `$${key}` }, 'missing'] }] } - } - }) - sort[`sort_isNull_${key}`] = options.sort[_key] === SortingOrder.Ascending ? 1 : -1 - sort[key] = options.sort[_key] === SortingOrder.Ascending ? 1 : -1 -} -function fillCustomSort ( - rules: SortingRules, - key: string, - pipeline: any[], - sort: any, - options: FindOptions, - _key: string -): void { - const branches = rules.cases.map((selector) => { - if (typeof selector.query === 'object') { - const q = selector.query as SortQuerySelector - if (q.$in !== undefined) { - return { case: { $in: { [key]: q.$in } }, then: selector.index } - } - if (q.$nin !== undefined) { - return { case: { $nin: { [key]: q.$in } }, then: selector.index } - } - if (q.$ne !== undefined) { - return { case: { $ne: [`$${key}`, q.$ne] }, then: selector.index } - } - } - return { case: { $eq: [`$${key}`, selector.query] }, then: selector.index } - }) - pipeline.push({ - $addFields: { - [`sort_${key}`]: { - $switch: { - branches, - default: rules.default ?? branches.length - } - } - } - }) - if (options.sort === undefined) { - options.sort = {} - } - sort[`sort_${key}`] = rules.order === SortingOrder.Ascending ? 1 : -1 -} - -function translateLikeQuery (pattern: string): { $regex: string, $options: string } { - return { - $regex: `^${pattern - .split('%') - .map((it) => escapeLikeForRegexp(it)) - .join('.*')}$`, - $options: 'i' - } -} - -/** - * @public - */ -export async function createMongoAdapter ( - ctx: MeasureContext, - hierarchy: Hierarchy, - url: string, - workspaceId: WorkspaceIds, - modelDb: ModelDb, - storage?: StorageAdapter, - options?: DbAdapterOptions -): Promise { - const client = getMongoClient(url) - const db = getWorkspaceMongoDB(await client.getClient(), workspaceId.dataId ?? workspaceId.uuid) - - return new MongoAdapter(db, hierarchy, modelDb, client, options) -} - -/** - * @public - */ -export async function createMongoTxAdapter ( - ctx: MeasureContext, - hierarchy: Hierarchy, - url: string, - workspaceId: WorkspaceIds, - modelDb: ModelDb -): Promise { - const client = getMongoClient(url) - const db = getWorkspaceMongoDB(await client.getClient(), workspaceId.dataId ?? workspaceId.uuid) - - return new MongoTxAdapter(db, hierarchy, modelDb, client) -} diff --git a/server/mongo/src/utils.ts b/server/mongo/src/utils.ts deleted file mode 100644 index f1fc4105c44..00000000000 --- a/server/mongo/src/utils.ts +++ /dev/null @@ -1,231 +0,0 @@ -// -// Copyright © 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { generateId, type Doc, type Domain, type FieldIndexConfig } from '@hcengineering/core' -import { PlatformError, unknownStatus } from '@hcengineering/platform' -import { type DomainHelperOperations } from '@hcengineering/server-core' -import { MongoClient, type Collection, type Db, type Document } from 'mongodb' - -const connections = new Map() - -const clientRefs = new Map() - -/** - * @public - */ -export async function shutdownMongo (): Promise { - for (const it of Array.from(clientRefs.values())) { - console.error((it as any).stack) - } - for (const c of connections.values()) { - c.close(true) - } - connections.clear() -} - -export interface MongoClientReference { - getClient: () => Promise - close: () => void -} - -class MongoClientReferenceImpl { - count: number - client: MongoClient | Promise - - constructor ( - client: MongoClient | Promise, - readonly onclose: () => void - ) { - this.count = 0 - this.client = client - } - - async getClient (): Promise { - if (this.client instanceof Promise) { - this.client = await this.client - } - return this.client - } - - close (force: boolean = false): void { - this.count-- - if (this.count === 0 || force) { - if (force) { - this.count = 0 - } - this.onclose() - void (async () => { - let cl = this.client - if (cl instanceof Promise) { - cl = await cl - } - await cl.close() - })() - } - } - - addRef (): void { - this.count++ - } -} -export class ClientRef implements MongoClientReference { - id = generateId() - stack = new Error().stack - constructor (readonly client: MongoClientReferenceImpl) { - clientRefs.set(this.id, this) - } - - closed = false - async getClient (): Promise { - if (!this.closed) { - return await this.client.getClient() - } else { - throw new PlatformError(unknownStatus('Mongo client is already closed')) - } - } - - close (): void { - // Do not allow double close of mongo connection client - if (!this.closed) { - clientRefs.delete(this.id) - this.closed = true - this.client.close() - } - } -} - -/** - * Initialize a workspace connection to DB - * @public - */ -export function getMongoClient (uri: string): MongoClientReference { - const extraOptions = JSON.parse(process.env.MONGO_OPTIONS ?? '{}') - const key = `${uri}${process.env.MONGO_OPTIONS ?? '{}'}` - let existing = connections.get(key) - - // If not created or closed - if (existing === undefined) { - existing = new MongoClientReferenceImpl( - MongoClient.connect(uri, { - retryReads: true, - appName: 'transactor', - enableUtf8Validation: false, - - ...extraOptions - }), - () => { - connections.delete(key) - } - ) - connections.set(key, existing) - } - // Add reference and return once closable - existing.addRef() - return new ClientRef(existing) -} - -/** - * @public - * - * Construct MongoDB table from workspace. - */ -export function getWorkspaceMongoDB (client: MongoClient, dbName: string): Db { - return client.db(dbName) -} - -export class DBCollectionHelper implements DomainHelperOperations { - collections = new Map>() - constructor (readonly db: Db) {} - - async listDomains (): Promise> { - const collections = await this.db.listCollections({}, { nameOnly: true }).toArray() - return new Set(collections.map((it) => it.name as unknown as Domain)) - } - - async init (domain?: Domain): Promise { - // Check and create DB if missin - if (domain === undefined) { - // Init existing collecfions - for (const c of (await this.db.listCollections({}, { nameOnly: true }).toArray()).map((it) => it.name)) { - this.collections.set(c, this.db.collection(c)) - } - } else { - this.collections.set(domain, this.db.collection(domain)) - } - } - - collection(domain: Domain): Collection { - let info = this.collections.get(domain) - if (info === undefined) { - info = this.db.collection(domain as string) - this.collections.set(domain, info) - } - return info - } - - async create (domain: Domain): Promise { - if (this.collections.get(domain) === undefined) { - const coll = this.db.collection(domain as string) - this.collections.set(domain, coll) - - while (true) { - const exists = await this.db.listCollections({ name: domain }).next() - if (exists === undefined) { - console.log('check connection to be created', domain) - await new Promise((resolve) => { - setTimeout(resolve) - }) - } else { - break - } - } - } - } - - async exists (domain: Domain): Promise { - return this.collections.has(domain) - } - - async createIndex (domain: Domain, value: string | FieldIndexConfig, options?: { name: string }): Promise { - if (typeof value === 'string') { - await this.collection(domain).createIndex(value, options) - } else { - if (value.filter !== undefined) { - await this.collection(domain).createIndex(value.keys, { - ...options, - partialFilterExpression: value.filter - }) - } else { - await this.collection(domain).createIndex(value.keys, { ...options, sparse: value.sparse ?? false }) - } - } - } - - async dropIndex (domain: Domain, name: string): Promise { - await this.collection(domain).dropIndex(name) - } - - async listIndexes (domain: Domain): Promise<{ name: string }[]> { - return await this.collection(domain).listIndexes().toArray() - } - - async estimatedCount (domain: Domain): Promise { - if (await this.exists(domain)) { - const c = this.collection(domain) - return await c.estimatedDocumentCount() - } - return 0 - } -} diff --git a/server/mongo/tsconfig.json b/server/mongo/tsconfig.json deleted file mode 100644 index c6a877cf6c3..00000000000 --- a/server/mongo/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/postgres/.eslintrc.js b/server/postgres/.eslintrc.js deleted file mode 100644 index ce90fb9646f..00000000000 --- a/server/postgres/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/postgres/.npmignore b/server/postgres/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/postgres/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/postgres/config/rig.json b/server/postgres/config/rig.json deleted file mode 100644 index 78cc5a17334..00000000000 --- a/server/postgres/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/server/postgres/jest.config.js b/server/postgres/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/postgres/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/postgres/migrations/allSchema.sql b/server/postgres/migrations/allSchema.sql deleted file mode 100644 index 7192737d2b4..00000000000 --- a/server/postgres/migrations/allSchema.sql +++ /dev/null @@ -1,37 +0,0 @@ -DO $$ -DECLARE - tbl_name text; - hash_col_not_exists boolean; - data_col_exists boolean; -BEGIN - FOR tbl_name IN - SELECT table_name - FROM information_schema.tables - WHERE table_schema = 'public' - AND table_type = 'BASE TABLE' - LOOP - EXECUTE format(' - SELECT EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_name = %L - AND column_name = ''data'' - AND data_type = ''jsonb'' - );', tbl_name) INTO data_col_exists; - - EXECUTE format(' - SELECT NOT EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_name = %L - AND column_name = ''"%%hash%%"'' - );', tbl_name) INTO hash_col_not_exists; - - IF data_col_exists AND hash_col_not_exists THEN - EXECUTE format(' - ALTER TABLE %I ADD COLUMN "%%hash%%" text;', tbl_name); - EXECUTE format(' - UPDATE %I SET "%%hash%%" = data->>''%%hash%%'';', tbl_name); - END IF; - END LOOP; -END $$; \ No newline at end of file diff --git a/server/postgres/migrations/calendarSchema.sql b/server/postgres/migrations/calendarSchema.sql deleted file mode 100644 index ea9a714ed53..00000000000 --- a/server/postgres/migrations/calendarSchema.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE calendar -ADD "hidden" bool; - -UPDATE calendar -SET "hidden" = (data->>'hidden')::boolean; diff --git a/server/postgres/migrations/dncSchema.sql b/server/postgres/migrations/dncSchema.sql deleted file mode 100644 index 0405e316992..00000000000 --- a/server/postgres/migrations/dncSchema.sql +++ /dev/null @@ -1,25 +0,0 @@ -ALTER TABLE notification_dnc -ADD "objectId" text, -ADD "objectClass" text, -ADD "user" text; - -ALTER TABLE notification_dnc -DROP COLUMN "attachedTo"; - -UPDATE notification_dnc -SET "objectId" = (data->>'objectId'); - -UPDATE notification_dnc -SET "objectClass" = (data->>'objectClass'); - -UPDATE notification_dnc -SET "user" = (data->>'user'); - -ALTER TABLE notification_dnc -ALTER COLUMN "objectId" SET NOT NULL; - -ALTER TABLE notification_dnc -ALTER COLUMN "objectClass" SET NOT NULL; - -ALTER TABLE notification_dnc -ALTER COLUMN "user" SET NOT NULL; \ No newline at end of file diff --git a/server/postgres/migrations/eventSchema.sql b/server/postgres/migrations/eventSchema.sql deleted file mode 100644 index 5a8c1ce9f02..00000000000 --- a/server/postgres/migrations/eventSchema.sql +++ /dev/null @@ -1,19 +0,0 @@ -ALTER TABLE event -ADD "date" bigint, -ADD "dueDate" bigint, -add calendar text, -ADD "participants" text[]; - -UPDATE event -SET "date" = (data->>'date')::bigint; - -UPDATE event -SET "dueDate" = (data->>'dueDate')::bigint; - -UPDATE calendar -SET "calendar" = (data->>'calendar'); - -UPDATE event -SET "participants" = array( - SELECT jsonb_array_elements_text(data->'participants') -); diff --git a/server/postgres/migrations/notificationSchema.sql b/server/postgres/migrations/notificationSchema.sql deleted file mode 100644 index 8bd87d53a90..00000000000 --- a/server/postgres/migrations/notificationSchema.sql +++ /dev/null @@ -1,25 +0,0 @@ -ALTER TABLE notification -ADD "isViewed" bool, -ADD archived bool, -ADD "user" text; - -ALTER TABLE notification -DROP COLUMN "attachedTo"; - -UPDATE notification -SET "isViewed" = (data->>'isViewed')::boolean; - -UPDATE notification -SET "archived" = (data->>'archived')::boolean; - -UPDATE notification -SET "user" = (data->>'user'); - -ALTER TABLE notification -ALTER COLUMN "isViewed" SET NOT NULL; - -ALTER TABLE notification -ALTER COLUMN archived SET NOT NULL; - -ALTER TABLE notification -ALTER COLUMN "user" SET NOT NULL; \ No newline at end of file diff --git a/server/postgres/migrations/spaceSchema.sql b/server/postgres/migrations/spaceSchema.sql deleted file mode 100644 index da86828c563..00000000000 --- a/server/postgres/migrations/spaceSchema.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE space -ADD "archived" bool; - -UPDATE space -SET "archived" = (data->>'archived')::boolean; diff --git a/server/postgres/migrations/timeSchema.sql b/server/postgres/migrations/timeSchema.sql deleted file mode 100644 index 3a6d7222938..00000000000 --- a/server/postgres/migrations/timeSchema.sql +++ /dev/null @@ -1,17 +0,0 @@ -ALTER TABLE time -ADD "workslots" bigint, -ADD "doneOn" bigint, -add rank text, -ADD "user" text; - -UPDATE time -SET "workslots" = (data->>'workslots')::bigint; - -UPDATE time -SET "doneOn" = (data->>'doneOn')::bigint; - -UPDATE time -SET "rank" = (data->>'rank'); - -UPDATE time -SET "user" = (data->>'user'); diff --git a/server/postgres/migrations/txSchema.sql b/server/postgres/migrations/txSchema.sql deleted file mode 100644 index 8818157fa8d..00000000000 --- a/server/postgres/migrations/txSchema.sql +++ /dev/null @@ -1,18 +0,0 @@ -ALTER TABLE tx -ADD "objectSpace" text, -ADD "objectId" text; - -UPDATE tx -SET "objectId" = (data->>'objectId'); - -UPDATE tx -SET "objectSpace" = (data->>'objectSpace'); - -ALTER TABLE tx -ALTER COLUMN "objectSpace" SET NOT NULL; - -ALTER TABLE tx -ADD "attachedTo" text; - -UPDATE tx -SET "attachedTo" = (data->>'attachedTo'); \ No newline at end of file diff --git a/server/postgres/migrations/uncSchema.sql b/server/postgres/migrations/uncSchema.sql deleted file mode 100644 index 125304c879f..00000000000 --- a/server/postgres/migrations/uncSchema.sql +++ /dev/null @@ -1,11 +0,0 @@ -ALTER TABLE notification_user -ADD "user" text; - -ALTER TABLE notification_user -DROP COLUMN "attachedTo"; - -UPDATE notification_user -SET "user" = (data->>'user'); - -ALTER TABLE notification_user -ALTER COLUMN "user" SET NOT NULL; \ No newline at end of file diff --git a/server/postgres/package.json b/server/postgres/package.json deleted file mode 100644 index 1dc3ae3a16a..00000000000 --- a/server/postgres/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@hcengineering/postgres", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Copyright © Hardcore Engineering Inc.", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/node": "^22.15.29" - }, - "dependencies": { - "postgres": "^3.4.7", - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "@hcengineering/postgres-base": "^0.7.6" - } -} diff --git a/server/postgres/src/__tests__/conversion.spec.ts b/server/postgres/src/__tests__/conversion.spec.ts deleted file mode 100644 index 47421a8199e..00000000000 --- a/server/postgres/src/__tests__/conversion.spec.ts +++ /dev/null @@ -1,208 +0,0 @@ -import core, { - Hierarchy, - MeasureMetricsContext, - ModelDb, - TxFactory, - type DocumentUpdate, - type PersonId, - type Ref, - type Space, - type Tx, - type WorkspaceUuid -} from '@hcengineering/core' -import { PostgresAdapter } from '../storage' -import { convertArrayParams, decodeArray, filterProjection } from '../utils' -import { genMinModel, test, type ComplexClass } from './minmodel' -import { createDummyClient, type TypedQuery } from './utils' -import { ConnectionMgr } from '@hcengineering/postgres-base' - -describe('array conversion', () => { - it('should handle undefined parameters', () => { - expect(convertArrayParams(undefined)).toBeUndefined() - }) - - it('should convert empty arrays', () => { - expect(convertArrayParams([['foo']])).toEqual(['{"foo"}']) - expect(convertArrayParams([[]])).toEqual(['{}']) - }) - - it('should handle string arrays with special characters', () => { - expect(convertArrayParams([['hello', 'world"quote"']])).toEqual(['{"hello","world\\"quote\\""}']) - }) - - it('should handle null values', () => { - expect(convertArrayParams([[null, 'value', null]])).toEqual(['{NULL,"value",NULL}']) - }) - - it('should handle mixed type arrays', () => { - expect(convertArrayParams([[123, 'text', null, true]])).toEqual(['{123,"text",NULL,true}']) - }) - - it('should pass through non-array parameters', () => { - expect(convertArrayParams(['text', 123, null])).toEqual(['text', 123, null]) - }) -}) - -describe('array decoding', () => { - it('should decode NULL to empty array', () => { - expect(decodeArray('NULL')).toEqual([]) - }) - - it('should decode empty array', () => { - expect(decodeArray('{}')).toEqual(['']) - }) - - it('should decode simple string array', () => { - expect(decodeArray('{hello,world}')).toEqual(['hello', 'world']) - }) - it('should decode encoded string array', () => { - expect(decodeArray('{"hello","world"}')).toEqual(['hello', 'world']) - }) - - it('should decode array with quoted strings', () => { - expect(decodeArray('{hello,"quoted value"}')).toEqual(['hello', 'quoted value']) - }) - - it('should decode array with escaped quotes', () => { - expect(decodeArray('{"hello \\"world\\""}')).toEqual(['hello "world"']) - }) - - it('should decode array with multiple escaped characters', () => { - expect(decodeArray('{"first \\"quote\\"","second \\"quote\\""}')).toEqual(['first "quote"', 'second "quote"']) - }) -}) - -const factory = new TxFactory('email:test' as PersonId) -function upd (id: string, partial: DocumentUpdate): Tx { - return factory.createTxUpdateDoc( - test.class.ComplexClass, - core.space.Workspace, - id as Ref, - partial - ) -} - -describe('query to sql conversion tests', () => { - it('check dummy db client', async () => { - const queries: TypedQuery[] = [] - const c = createDummyClient(queries) - - await c.execute('select now()') - expect(queries[0].query).toEqual('select now()') - }) - it('check simple update', async () => { - const { adapter, ctx, queries } = createTestContext() - - await adapter.tx( - ctx, - upd('obj1', { - stringField: 'test' - }) - ) - expect(queries[0].query).toEqual( - 'UPDATE pg_testing SET "modifiedBy" = update_data."_modifiedBy", "modifiedOn" = update_data."_modifiedOn", "%hash%" = update_data."_%hash%", data = COALESCE(data || update_data._data)\n FROM (values ($2::text, $3::text,$4::bigint,$5::text,$6::jsonb)) AS update_data(__id, "_modifiedBy","_modifiedOn","_%hash%","_data")\n WHERE "workspaceId" = $1::uuid AND "_id" = update_data.__id' - ) - }) - it('check space update', async () => { - const { adapter, ctx, queries } = createTestContext() - - await adapter.tx( - ctx, - upd('obj1', { - space: 'new-space' as Ref - }) - ) - expect(queries[0].query).toEqual( - 'UPDATE pg_testing SET "modifiedBy" = update_data."_modifiedBy", "modifiedOn" = update_data."_modifiedOn", "%hash%" = update_data."_%hash%", "space" = update_data."_space"\n FROM (values ($2::text, $3::text,$4::bigint,$5::text,$6::text)) AS update_data(__id, "_modifiedBy","_modifiedOn","_%hash%","_space")\n WHERE "workspaceId" = $1::uuid AND "_id" = update_data.__id' - ) - }) - it('check few documents update', async () => { - const { adapter, ctx, queries } = createTestContext() - - await adapter.tx( - ctx, - upd('obj1', { - stringField: 'test' - }), - upd('obj2', { - stringField: 'test2' - }), - upd('obj3', { - stringField: 'test' - }) - ) - expect(queries[0].query).toEqual( - 'UPDATE pg_testing SET "modifiedBy" = update_data."_modifiedBy", "modifiedOn" = update_data."_modifiedOn", "%hash%" = update_data."_%hash%", data = COALESCE(data || update_data._data)\n FROM (values ($2::text, $3::text,$4::bigint,$5::text,$6::jsonb),($7::text, $8::text,$9::bigint,$10::text,$11::jsonb),($12::text, $13::text,$14::bigint,$15::text,$16::jsonb)) AS update_data(__id, "_modifiedBy","_modifiedOn","_%hash%","_data")\n WHERE "workspaceId" = $1::uuid AND "_id" = update_data.__id' - ) - }) -}) -function createTestContext (): { adapter: PostgresAdapter, ctx: MeasureMetricsContext, queries: TypedQuery[] } { - const ctx = new MeasureMetricsContext('test', {}) - const queries: TypedQuery[] = [] - const c = createDummyClient(queries) - - const minModel = genMinModel() - const hierarchy = new Hierarchy() - for (const tx of minModel) { - hierarchy.tx(tx) - } - const modelDb = new ModelDb(hierarchy) - modelDb.addTxes(ctx, minModel, true) - const adapter = new PostgresAdapter( - c, - new ConnectionMgr(c), - { - url: () => 'test', - close: () => {} - }, - 'workspace' as WorkspaceUuid, - hierarchy, - modelDb, - 'test' - ) - return { adapter, ctx, queries } -} - -describe('projection', () => { - it('mixin query projection', () => { - const data = { - '638611f18894c91979399ef3': { - Источник_6386125d8894c91979399eff: 'Workable' - }, - attachments: 1, - avatar: null, - avatarProps: null, - avatarType: 'color', - channels: 3, - city: 'Poland', - docUpdateMessages: 31, - name: 'Mulkuha,Muklyi', - 'notification:mixin:Collaborators': { - collaborators: [] - }, - 'recruit:mixin:Candidate': { - Title_63f38419efefd99805238bbd: 'Backend-RoR', - Trash_64493626f9b50e77bf82d231: 'Нет', - __mixin: 'true', - applications: 1, - onsite: null, - remote: null, - skills: 18, - title: '', - Опытработы_63860d5c8894c91979399e73: '2018', - Уровеньанглийского_63860d038894c91979399e6f: 'UPPER' - } - } - const projected = filterProjection(data, { - 'recruit:mixin:Candidate.Уровеньанглийского_63860d038894c91979399e6f': 1, - _class: 1, - space: 1, - modifiedOn: 1 - }) - expect(projected).toEqual({ - 'recruit:mixin:Candidate': { - Уровеньанглийского_63860d038894c91979399e6f: 'UPPER' - } - }) - }) -}) diff --git a/server/postgres/src/__tests__/minmodel.ts b/server/postgres/src/__tests__/minmodel.ts deleted file mode 100644 index 2432eac880f..00000000000 --- a/server/postgres/src/__tests__/minmodel.ts +++ /dev/null @@ -1,282 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type AccountUuid, - type AnyAttribute, - type Arr, - type AttachedDoc, - type Class, - ClassifierKind, - type Data, - type Doc, - type Domain, - DOMAIN_MODEL, - DOMAIN_RELATION, - DOMAIN_TX, - type Mixin, - type Obj, - type PersonId, - type Ref, - type TxCreateDoc, - type TxCUD, - TxFactory -} from '@hcengineering/core' -import type { IntlString, Plugin } from '@hcengineering/platform' -import { plugin } from '@hcengineering/platform' -import { taskPlugin } from './tasks' - -export const txFactory = new TxFactory(core.account.System) - -export function createClass (_class: Ref>, attributes: Data>): TxCreateDoc { - return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class) -} - -export function createAttribute (attribute: Data): TxCreateDoc { - return txFactory.createTxCreateDoc(core.class.Attribute, core.space.Model, attribute) -} - -/** - * @public - */ -export function createDoc ( - _class: Ref>, - attributes: Data, - id?: Ref, - modifiedBy?: PersonId -): TxCreateDoc { - const result = txFactory.createTxCreateDoc(_class, core.space.Model, attributes, id) - if (modifiedBy !== undefined) { - result.modifiedBy = modifiedBy - } - return result -} - -/** - * @public - */ -export interface TestMixin extends Doc { - arr: Arr -} - -/** - * @public - */ -export interface AttachedComment extends AttachedDoc { - message: string -} - -export interface ComplexClass extends Doc { - stringField: string - numberField: number - booleanField: boolean - arrayField: string[] - numberArrayField: number[] -} - -export interface ComplexMixin extends Mixin { - stringField: string - numberField: number - booleanField: boolean - arrayField: string[] - numberArrayField: number[] -} - -/** - * @public - */ -export const test = plugin('test' as Plugin, { - mixin: { - TestMixin: '' as Ref>, - ComplexMixin: '' as Ref> - }, - class: { - TestComment: '' as Ref>, - ComplexClass: '' as Ref> - } -}) - -/** - * @public - * Generate minimal model for testing purposes. - * @returns R - */ -export function genMinModel (): TxCUD[] { - const txes = [] - // Fill Tx'es with basic model classes. - txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS })) - txes.push( - createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS }) - ) - txes.push( - createClass(core.class.Attribute, { - label: 'Attribute' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.Relation, { - label: 'Relation' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_RELATION - }) - ) - txes.push( - createClass(core.class.Association, { - label: 'Association' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - txes.push( - createClass(core.class.AttachedDoc, { - label: 'AttachedDoc' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.MIXIN - }) - ) - txes.push( - createClass(core.class.Class, { - label: 'Class' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - txes.push( - createClass(core.class.Space, { - label: 'Space' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - - txes.push( - createClass(core.class.Tx, { - label: 'Tx' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCUD, { - label: 'TxCUD' as IntlString, - extends: core.class.Tx, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCreateDoc, { - label: 'TxCreateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxUpdateDoc, { - label: 'TxUpdateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxRemoveDoc, { - label: 'TxRemoveDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - - txes.push( - createClass(test.mixin.TestMixin, { - label: 'TestMixin' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.MIXIN - }) - ) - - txes.push( - createClass(test.class.TestComment, { - label: 'TestComment' as IntlString, - extends: core.class.AttachedDoc, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(test.class.ComplexClass, { - label: 'ComplexClass' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: 'pg-testing' as Domain - }) - ) - - txes.push( - createClass(test.mixin.ComplexMixin, { - label: 'ComplexMixin' as IntlString, - extends: test.class.ComplexClass, - kind: ClassifierKind.MIXIN, - domain: 'pg-testing' as Domain - }) - ) - - const u1 = 'User1' as AccountUuid - const u2 = 'User2' as AccountUuid - txes.push( - createDoc(core.class.Space, { - name: 'Sp1', - description: '', - private: false, - archived: false, - members: [u1, u2] - }) - ) - - txes.push( - createDoc(core.class.Space, { - name: 'Sp2', - description: '', - private: false, - archived: false, - members: [u1] - }) - ) - - txes.push( - createClass(core.class.DomainIndexConfiguration, { - label: 'DomainIndexConfiguration' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - - txes.push( - createDoc(core.class.Association, { - nameA: 'my-assoc', - nameB: 'my-assoc', - classA: taskPlugin.class.Task, - classB: taskPlugin.class.Task, - type: '1:1' - }) - ) - return txes -} diff --git a/server/postgres/src/__tests__/storage.test.ts b/server/postgres/src/__tests__/storage.test.ts deleted file mode 100644 index 524d6d1e19c..00000000000 --- a/server/postgres/src/__tests__/storage.test.ts +++ /dev/null @@ -1,389 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import core, { - type Client, - createClient, - Hierarchy, - MeasureMetricsContext, - ModelDb, - type Ref, - SortingOrder, - type Space, - TxOperations, - type WorkspaceUuid -} from '@hcengineering/core' -import { type DbAdapter, wrapAdapterToClient } from '@hcengineering/server-core' -import { - createPostgresAdapter, - createPostgresTxAdapter, - getDBClient, - shutdownPostgres, - type PostgresClientReference -} from '..' -import { genMinModel } from './minmodel' -import { createTaskModel, type Task, type TaskComment, taskPlugin } from './tasks' - -const txes = genMinModel() - -createTaskModel(txes) - -const contextVars: Record = {} - -describe('postgres operations', () => { - const baseDbUri: string = process.env.DB_URL ?? 'postgresql://root@localhost:26257/defaultdb?sslmode=disable' - let dbUuid = crypto.randomUUID() as WorkspaceUuid - let dbUri: string = baseDbUri.replace('defaultdb', dbUuid) - const clientRef: PostgresClientReference = getDBClient(baseDbUri) - let hierarchy: Hierarchy - let model: ModelDb - let client: Client - let operations: TxOperations - let serverStorage: DbAdapter | undefined - - afterAll(async () => { - clientRef.close() - await shutdownPostgres() - }) - - beforeEach(async () => { - try { - dbUuid = crypto.randomUUID() as WorkspaceUuid - dbUri = baseDbUri.replace('defaultdb', dbUuid) - const client = await clientRef.getClient() - await client`CREATE DATABASE ${client(dbUuid)}` - } catch (err) { - console.error(err) - } - - jest.setTimeout(30000) - await initDb() - }) - - afterEach(async () => { - try { - // await client.close() - // await (await clientRef.getClient()).query(`DROP DATABASE ${dbId}`) - } catch (err) { - console.log(err) - } - await serverStorage?.close() - }) - - async function initDb (): Promise { - // Remove all stuff from database. - hierarchy = new Hierarchy() - model = new ModelDb(hierarchy) - for (const t of txes) { - hierarchy.tx(t) - } - for (const t of txes) { - await model.tx(t) - } - - const mctx = new MeasureMetricsContext('', {}) - const txStorage = await createPostgresTxAdapter( - mctx, - hierarchy, - dbUri, - { - uuid: dbUuid, - url: dbUri - }, - model - ) - - // Put all transactions to Tx - for (const t of txes) { - await txStorage.tx(mctx, t) - } - - await txStorage.close() - - const ctx = new MeasureMetricsContext('client', {}) - const serverStorage = await createPostgresAdapter( - ctx, - hierarchy, - dbUri, - { - uuid: dbUuid, - url: dbUri - }, - model - ) - await serverStorage.init?.(ctx, contextVars) - client = await createClient(async (handler) => { - return wrapAdapterToClient(ctx, serverStorage, txes) - }) - - operations = new TxOperations(client, core.account.System) - } - - it('check add', async () => { - const times: number[] = [] - for (let i = 0; i < 50; i++) { - const t = Date.now() - await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: `my-task-${i}`, - description: `${i * i}`, - rate: 20 + i - }) - times.push(Date.now() - t) - } - - console.log('createDoc times', times) - - const r = await client.findAll(taskPlugin.class.Task, {}) - expect(r.length).toEqual(50) - }) - - it('check find by criteria', async () => { - jest.setTimeout(20000) - for (let i = 0; i < 50; i++) { - await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: `my-task-${i}`, - description: `${i * i}`, - rate: 20 + i, - arr: new Array(i).fill(i) - }) - } - - const r = await client.findAll(taskPlugin.class.Task, {}) - expect(r.length).toEqual(50) - - const first = await client.findAll(taskPlugin.class.Task, { name: 'my-task-0' }) - expect(first.length).toEqual(1) - - const second = await client.findAll(taskPlugin.class.Task, { name: { $like: '%0' } }) - expect(second.length).toEqual(5) - - const third = await client.findAll(taskPlugin.class.Task, { rate: { $in: [25, 26, 27, 28] } }) - expect(third.length).toEqual(4) - - const size = await client.findAll(taskPlugin.class.Task, { arr: { $size: 5 } }) - expect(size.length).toEqual(1) - - const sizeGt = await client.findAll(taskPlugin.class.Task, { arr: { $size: { $gt: 45 } } }) - expect(sizeGt.length).toEqual(4) - - const sizeGte = await client.findAll(taskPlugin.class.Task, { arr: { $size: { $gte: 45 } } }) - expect(sizeGte.length).toEqual(5) - - const sizeLt = await client.findAll(taskPlugin.class.Task, { arr: { $size: { $lt: 45 } } }) - expect(sizeLt.length).toEqual(45) - - const sizeLte = await client.findAll(taskPlugin.class.Task, { arr: { $size: { $lte: 45 } } }) - expect(sizeLte.length).toEqual(46) - }) - - it('check update', async () => { - await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: 'my-task', - description: 'some data ', - rate: 20, - arr: [] - }) - - const doc = (await client.findAll(taskPlugin.class.Task, {}))[0] - await operations.updateDoc(doc._class, doc.space, doc._id, { rate: null }) - let tasks = await client.findAll(taskPlugin.class.Task, {}) - expect(tasks.length).toEqual(1) - expect(tasks[0].rate).toBeNull() - - await operations.updateDoc(doc._class, doc.space, doc._id, { rate: 30 }) - tasks = await client.findAll(taskPlugin.class.Task, {}) - expect(tasks.length).toEqual(1) - expect(tasks[0].rate).toEqual(30) - - await operations.updateDoc(doc._class, doc.space, doc._id, { $inc: { rate: 1 } }) - tasks = await client.findAll(taskPlugin.class.Task, {}) - expect(tasks.length).toEqual(1) - expect(tasks[0].rate).toEqual(31) - - await operations.updateDoc(doc._class, doc.space, doc._id, { $inc: { rate: -1 } }) - tasks = await client.findAll(taskPlugin.class.Task, {}) - expect(tasks.length).toEqual(1) - expect(tasks[0].rate).toEqual(30) - - await operations.updateDoc(doc._class, doc.space, doc._id, { $push: { arr: 1 } }) - tasks = await client.findAll(taskPlugin.class.Task, {}) - expect(tasks.length).toEqual(1) - expect(tasks[0].arr?.length).toEqual(1) - expect(tasks[0].arr?.[0]).toEqual(1) - - await operations.updateDoc(doc._class, doc.space, doc._id, { $push: { arr: 3 } }) - tasks = await client.findAll(taskPlugin.class.Task, {}) - expect(tasks.length).toEqual(1) - expect(tasks[0].arr?.length).toEqual(2) - expect(tasks[0].arr?.[0]).toEqual(1) - expect(tasks[0].arr?.[1]).toEqual(3) - }, 1000000) - - it('check remove', async () => { - for (let i = 0; i < 10; i++) { - await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: `my-task-${i}`, - description: `${i * i}`, - rate: 20 + i - }) - } - - let r = await client.findAll(taskPlugin.class.Task, {}) - expect(r.length).toEqual(10) - await operations.removeDoc(taskPlugin.class.Task, '' as Ref, r[0]._id) - r = await client.findAll(taskPlugin.class.Task, {}) - expect(r.length).toEqual(9) - }) - - it('limit and sorting', async () => { - for (let i = 0; i < 5; i++) { - await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: `my-task-${i}`, - description: `${i * i}`, - rate: 20 + i - }) - } - - const without = await client.findAll(taskPlugin.class.Task, {}) - expect(without).toHaveLength(5) - - const limit = await client.findAll(taskPlugin.class.Task, {}, { limit: 1 }) - expect(limit).toHaveLength(1) - - const sortAsc = await client.findAll(taskPlugin.class.Task, {}, { sort: { name: SortingOrder.Ascending } }) - expect(sortAsc[0].name).toMatch('my-task-0') - - const sortDesc = await client.findAll(taskPlugin.class.Task, {}, { sort: { name: SortingOrder.Descending } }) - expect(sortDesc[0].name).toMatch('my-task-4') - - const sortEmpty = await client.findAll(taskPlugin.class.Task, {}, { sort: {} }) - expect(sortEmpty).toHaveLength(5) - }) - - it('check attached', async () => { - const docId = await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: 'my-task', - description: 'Descr', - rate: 20 - }) - - const commentId = await operations.addCollection( - taskPlugin.class.TaskComment, - '' as Ref, - docId, - taskPlugin.class.Task, - 'tasks', - { - message: 'my-msg', - date: new Date() - } - ) - - await operations.addCollection( - taskPlugin.class.TaskComment, - '' as Ref, - docId, - taskPlugin.class.Task, - 'tasks', - { - message: 'my-msg2', - date: new Date() - } - ) - - const r2 = await client.findAll( - taskPlugin.class.TaskComment, - {}, - { - lookup: { - attachedTo: taskPlugin.class.Task - } - } - ) - expect(r2.length).toEqual(2) - expect((r2[0].$lookup?.attachedTo as Task)?._id).toEqual(docId) - - const r3 = await client.findAll( - taskPlugin.class.Task, - {}, - { - lookup: { - _id: { comment: taskPlugin.class.TaskComment } - } - } - ) - - expect(r3).toHaveLength(1) - expect((r3[0].$lookup as any).comment).toHaveLength(2) - - const comment2Id = await operations.addCollection( - taskPlugin.class.TaskComment, - '' as Ref, - commentId, - taskPlugin.class.TaskComment, - 'comments', - { - message: 'my-msg3', - date: new Date() - } - ) - - const r4 = await client.findAll( - taskPlugin.class.TaskComment, - { - _id: comment2Id - }, - { - lookup: { attachedTo: [taskPlugin.class.TaskComment, { attachedTo: taskPlugin.class.Task } as any] } - } - ) - expect((r4[0].$lookup?.attachedTo as TaskComment)?._id).toEqual(commentId) - expect(((r4[0].$lookup?.attachedTo as any)?.$lookup.attachedTo as Task)?._id).toEqual(docId) - }) - - it('check associations', async () => { - const association = await operations.findOne(core.class.Association, {}) - if (association == null) { - throw new Error('Association not found') - } - - const firstTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: 'my-task', - description: 'Descr', - rate: 20 - }) - - const secondTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref, { - name: 'my-task2', - description: 'Descr', - rate: 20 - }) - - await operations.createDoc(core.class.Relation, '' as Ref, { - docA: firstTask, - docB: secondTask, - association: association._id - }) - - const r = await client.findAll( - taskPlugin.class.Task, - { _id: firstTask }, - { - associations: [[association._id, 1]] - } - ) - expect(r.length).toEqual(1) - expect((r[0].$associations?.[association._id][0] as unknown as Task)?._id).toEqual(secondTask) - }) -}) diff --git a/server/postgres/src/__tests__/tasks.ts b/server/postgres/src/__tests__/tasks.ts deleted file mode 100644 index 7b7c9064efb..00000000000 --- a/server/postgres/src/__tests__/tasks.ts +++ /dev/null @@ -1,121 +0,0 @@ -import core, { - type AttachedDoc, - type Class, - ClassifierKind, - type Data, - type Doc, - type Domain, - type PersonId, - type Ref, - type Space, - type Tx -} from '@hcengineering/core' -import { type IntlString, plugin, type Plugin } from '@hcengineering/platform' -import { createAttribute, createClass } from './minmodel' - -export interface TaskComment extends AttachedDoc { - message: string - date: Date -} - -export enum TaskStatus { - Open, - Close, - Resolved = 100, - InProgress -} - -export enum TaskReproduce { - Always = 'always', - Rare = 'rare', - Sometimes = 'sometimes' -} - -export interface Task extends Doc { - name: string - description: string - rate?: number | null - status?: TaskStatus - reproduce?: TaskReproduce - eta?: TaskEstimate | null - arr?: number[] -} - -/** - * Define ROM and Estimated Time to arrival - */ -export interface TaskEstimate extends AttachedDoc { - rom: number // in hours - eta: number // in hours -} - -export interface TaskMixin extends Task { - textValue?: string -} - -export interface TaskWithSecond extends Task { - secondTask: string | null -} - -const taskIds = 'taskIds' as Plugin - -export const taskPlugin = plugin(taskIds, { - class: { - Task: '' as Ref>, - TaskEstimate: '' as Ref>, - TaskComment: '' as Ref> - } -}) - -/** - * Create a random task with name specified - * @param name - */ -export function createTask (name: string, rate: number, description: string): Data { - return { - name, - description, - rate - } -} - -export const doc1: Task = { - _id: 'd1' as Ref, - _class: taskPlugin.class.Task, - name: 'my-space', - description: 'some-value', - rate: 20, - modifiedBy: 'user' as PersonId, - modifiedOn: 10, - // createdOn: 10, - space: '' as Ref -} - -export function createTaskModel (txes: Tx[]): void { - txes.push( - createClass(taskPlugin.class.Task, { - kind: ClassifierKind.CLASS, - label: 'Task' as IntlString, - domain: 'test-task' as Domain - }), - createClass(taskPlugin.class.TaskEstimate, { - kind: ClassifierKind.CLASS, - label: 'Estimate' as IntlString, - domain: 'test-task' as Domain - }), - createClass(taskPlugin.class.TaskComment, { - kind: ClassifierKind.CLASS, - label: 'Comment' as IntlString, - domain: 'test-task' as Domain - }), - createAttribute({ - attributeOf: taskPlugin.class.Task, - name: 'arr', - type: { - _class: core.class.ArrOf, - label: 'arr' as IntlString, - type: core.class.TypeNumber - } - }) - ) -} diff --git a/server/postgres/src/__tests__/utils.ts b/server/postgres/src/__tests__/utils.ts deleted file mode 100644 index 02661834377..00000000000 --- a/server/postgres/src/__tests__/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { DBClient } from '@hcengineering/postgres-base' - -export interface TypedQuery { - query: string - params?: any[] -} -export function createDummyClient (queries: TypedQuery[]): DBClient { - const client: DBClient = { - execute: async (query, params) => { - queries.push({ query, params }) - return Object.assign([], { count: 0 }) - }, - raw: () => jest.fn() as any, - reserve: async () => client, - release: jest.fn() - } - return client -} diff --git a/server/postgres/src/index.ts b/server/postgres/src/index.ts deleted file mode 100644 index 9e6646e1da1..00000000000 --- a/server/postgres/src/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { getDBClient, retryTxn } from '@hcengineering/postgres-base' -import type { WorkspaceDestroyAdapter } from '@hcengineering/server-core' -import { domainSchemas } from './schemas' - -export { getDocFieldsByDomains, translateDomain } from './schemas' -export * from './storage' -export { convertDoc, createTables } from './utils' - -export * from '@hcengineering/postgres-base' - -export function createPostgreeDestroyAdapter (url: string): WorkspaceDestroyAdapter { - return { - deleteWorkspace: async (ctx, workspaceUuid): Promise => { - const client = getDBClient(url) - try { - if (workspaceUuid == null) { - throw new Error('Workspace uuid is not defined') - } - const connection = await client.getClient() - - await ctx.with( - 'delete-workspace', - {}, - async (ctx) => { - // We need to clear information about workspace from all collections in schema - for (const [domain] of Object.entries(domainSchemas)) { - await ctx.with( - 'delete-workspace-domain', - {}, - async (ctx) => { - await retryTxn(connection, async (client) => { - await client.unsafe(`delete from ${domain} where "workspaceId" = $1::uuid`, [workspaceUuid]) - }) - }, - { domain } - ) - } - }, - { url: workspaceUuid } - ) - } catch (err: any) { - ctx.error('failed to clean workspace data', { err }) - throw err - } finally { - client.close() - } - } - } -} diff --git a/server/postgres/src/schemas.ts b/server/postgres/src/schemas.ts deleted file mode 100644 index a96cc984732..00000000000 --- a/server/postgres/src/schemas.ts +++ /dev/null @@ -1,354 +0,0 @@ -import { DOMAIN_COLLABORATOR, DOMAIN_MODEL_TX, DOMAIN_RELATION, DOMAIN_SPACE, DOMAIN_TX } from '@hcengineering/core' - -export type DataType = 'bigint' | 'bool' | 'text' | 'text[]' - -export function getIndex (field: FieldSchema): string { - if (field.indexType === undefined || field.indexType === 'btree') { - return '' - } - return ` USING ${field.indexType}` -} - -export interface FieldSchema { - type: DataType - notNull: boolean - index: boolean - indexType?: 'btree' | 'gin' | 'gist' | 'brin' | 'hash' -} - -export type Schema = Record - -const baseSchema: Schema = { - _id: { - type: 'text', - notNull: true, - index: false - }, - _class: { - type: 'text', - notNull: true, - index: true - }, - space: { - type: 'text', - notNull: true, - index: true - }, - modifiedBy: { - type: 'text', - notNull: true, - index: false - }, - createdBy: { - type: 'text', - notNull: false, - index: false - }, - modifiedOn: { - type: 'bigint', - notNull: true, - index: false - }, - createdOn: { - type: 'bigint', - notNull: false, - index: false - }, - '%hash%': { - type: 'text', - notNull: false, - index: false - } -} - -const defaultSchema: Schema = { - ...baseSchema, - attachedTo: { - type: 'text', - notNull: false, - index: true - } -} - -const collaboratorSchema: Schema = { - ...baseSchema, - attachedTo: { - type: 'text', - notNull: true, - index: true - }, - attachedToClass: { - type: 'text', - notNull: true, - index: true - }, - collaborator: { - type: 'text', - notNull: true, - index: true - } -} - -const spaceSchema: Schema = { - ...baseSchema, - private: { - type: 'bool', - notNull: true, - index: true - }, - members: { - type: 'text[]', - notNull: true, - index: true, - indexType: 'gin' - }, - archived: { - type: 'bool', - notNull: true, - index: true - } -} - -const relationSchema: Schema = { - ...baseSchema, - docA: { - type: 'text', - notNull: true, - index: true - }, - docB: { - type: 'text', - notNull: true, - index: true - }, - association: { - type: 'text', - notNull: true, - index: true - } -} - -const txSchema: Schema = { - ...defaultSchema, - objectSpace: { - type: 'text', - notNull: true, - index: true - }, - objectId: { - type: 'text', - notNull: false, - index: false - } -} - -const notificationSchema: Schema = { - ...baseSchema, - isViewed: { - type: 'bool', - notNull: true, - index: true - }, - archived: { - type: 'bool', - notNull: true, - index: true - }, - user: { - type: 'text', - notNull: true, - index: true - } -} - -const dncSchema: Schema = { - ...baseSchema, - objectId: { - type: 'text', - notNull: true, - index: true - }, - objectClass: { - type: 'text', - notNull: true, - index: false - }, - user: { - type: 'text', - notNull: true, - index: true - } -} - -const userNotificationSchema: Schema = { - ...baseSchema, - user: { - type: 'text', - notNull: true, - index: true - } -} - -const timeSchema: Schema = { - ...baseSchema, - workslots: { - type: 'bigint', - notNull: false, - index: true - }, - doneOn: { - type: 'bigint', - notNull: false, - index: true - }, - user: { - type: 'text', - notNull: true, - index: true - }, - rank: { - type: 'text', - notNull: true, - index: false - } -} - -const calendarSchema: Schema = { - ...baseSchema, - hidden: { - type: 'bool', - notNull: true, - index: true - } -} - -const eventSchema: Schema = { - ...defaultSchema, - calendar: { - type: 'text', - notNull: true, - index: true - }, - date: { - type: 'bigint', - notNull: true, - index: true - }, - dueDate: { - type: 'bigint', - notNull: true, - index: true - }, - participants: { - type: 'text[]', - notNull: true, - index: true - } -} - -const docSyncInfo: Schema = { - ...baseSchema, - needSync: { - type: 'text', - notNull: false, - index: false - }, - externalVersion: { - type: 'text', - notNull: false, - index: false - }, - repository: { - type: 'text', - notNull: false, - index: false - }, - url: { - type: 'text', - notNull: false, - index: false - }, - parent: { - type: 'text', - notNull: false, - index: false - }, - objectClass: { - type: 'text', - notNull: false, - index: false - }, - deleted: { - type: 'bool', - notNull: false, - index: false - } -} - -const githubLogin: Schema = { - ...baseSchema, - login: { - type: 'text', - notNull: true, - index: true - } -} - -export function addSchema (domain: string, schema: Schema): void { - domainSchemas[translateDomain(domain)] = schema - domainSchemaFields.set(domain, createSchemaFields(schema)) -} - -export function translateDomain (domain: string): string { - return domain.replaceAll('-', '_') -} - -export const domainSchemas: Record = { - [DOMAIN_SPACE]: spaceSchema, - [DOMAIN_TX]: txSchema, - [DOMAIN_MODEL_TX]: txSchema, - [translateDomain('time')]: timeSchema, - [translateDomain('calendar')]: calendarSchema, - [translateDomain('event')]: eventSchema, - notification: notificationSchema, - [translateDomain('notification-dnc')]: dncSchema, - [translateDomain('notification-user')]: userNotificationSchema, - [translateDomain('github_sync')]: docSyncInfo, - [translateDomain('github_user')]: githubLogin, - [DOMAIN_RELATION]: relationSchema, - [DOMAIN_COLLABORATOR]: collaboratorSchema, - kanban: defaultSchema -} - -export function getSchema (domain: string): Schema { - return domainSchemas[translateDomain(domain)] ?? defaultSchema -} - -export function getDocFieldsByDomains (domain: string): string[] { - const schema = domainSchemas[translateDomain(domain)] ?? defaultSchema - return Object.keys(schema) -} - -export interface SchemaAndFields { - schema: Schema - - fields: string[] - domainFields: Set -} - -function createSchemaFields (schema: Schema): SchemaAndFields { - const fields = Object.keys(schema) - const domainFields = new Set(Object.keys(schema)) - return { schema, fields, domainFields } -} - -const defaultSchemaFields: SchemaAndFields = createSchemaFields(defaultSchema) - -const domainSchemaFields = new Map() -for (const [domain, _schema] of Object.entries(domainSchemas)) { - domainSchemaFields.set(domain, createSchemaFields(_schema)) -} - -export function getSchemaAndFields (domain: string): SchemaAndFields { - return domainSchemaFields.get(translateDomain(domain)) ?? defaultSchemaFields -} diff --git a/server/postgres/src/storage.ts b/server/postgres/src/storage.ts deleted file mode 100644 index 9de83ebf373..00000000000 --- a/server/postgres/src/storage.ts +++ /dev/null @@ -1,2119 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - AccountRole, - type AssociationQuery, - type Class, - type Doc, - type DocInfo, - type DocumentQuery, - type DocumentUpdate, - type Domain, - DOMAIN_COLLABORATOR, - DOMAIN_MODEL, - DOMAIN_MODEL_TX, - DOMAIN_RELATION, - DOMAIN_SPACE, - DOMAIN_TX, - type FindOptions, - type FindResult, - groupByArray, - type Hierarchy, - isOperator, - type Iterator, - type Lookup, - type MeasureContext, - type ModelDb, - type ObjQueryType, - type Projection, - RateLimiter, - type Ref, - type ReverseLookups, - type SessionData, - shouldShowArchived, - type SortingQuery, - type StorageIterator, - systemAccountUuid, - toFindResult, - type Tx, - type TxCreateDoc, - type TxCUD, - type TxMixin, - TxProcessor, - type TxRemoveDoc, - type TxResult, - type TxUpdateDoc, - withContext, - type WithLookup, - type WorkspaceIds, - type WorkspaceUuid, - getClassCollaborators -} from '@hcengineering/core' -import { - type ConnectionMgr, - createDBClient, - type DBClient, - doFetchTypes, - getDBClient -} from '@hcengineering/postgres-base' -import { - calcHashHash, - type DbAdapter, - type DbAdapterHandler, - type DomainHelperOperations, - type RawFindIterator, - type ServerFindOptions, - type TxAdapter -} from '@hcengineering/server-core' -import type postgres from 'postgres' -import { - getDocFieldsByDomains, - getSchema, - getSchemaAndFields, - type Schema, - type SchemaAndFields, - translateDomain -} from './schemas' -import { type ValueType } from './types' -import { - convertArrayParams, - convertDoc, - createTables, - DBCollectionHelper, - type DBDoc, - escape, - filterProjection, - inferType, - isDataField, - isOwner, - type JoinProps, - NumericTypes, - parseDoc, - parseDocWithProjection, - parseUpdate -} from './utils' -async function * createCursorGenerator ( - client: postgres.Sql, - sql: string, - params: any, - schema: Schema, - bulkSize = 1000 -): AsyncGenerator { - const cursor = client.unsafe(sql, doFetchTypes ? params : convertArrayParams(params)).cursor(bulkSize) - try { - let docs: Doc[] = [] - for await (const part of cursor) { - docs.push(...part.filter((it) => it != null).map((it) => parseDoc(it as any, schema))) - if (docs.length > 0) { - yield docs - docs = [] - } - } - if (docs.length > 0) { - yield docs - docs = [] - } - } catch (err: any) { - console.error('failed to recieve data', { err }) - throw err // Rethrow the error after logging - } -} - -class ValuesVariables { - index: number = 1 - values: any[] = [] - - valueHashes = new Map() - - add (value: any, type: string = ''): string { - // Compact value if string and same - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - const vkey = `${value}:${type}` - const v = this.valueHashes.get(vkey) - if (v !== undefined) { - return v - } - this.values.push(value) - const idx = type !== '' ? `$${this.index++}${type}` : `$${this.index++}` - this.valueHashes.set(vkey, idx) - return idx - } else { - this.values.push(value) - return type !== '' ? `$${this.index++}${type}` : `$${this.index++}` - } - } - - getValues (): any[] { - return this.values - } - - addArray (value: any[], type: string = ''): string { - return this.add( - value.filter((it) => it != null), - type - ) - } - - addArrayI (value: any[], type: string = ''): string { - const vals = value.filter((it) => it != null) - if (vals.length === 0) { - return "array['NULL']" - } - return this.add(vals, type) - } - - injectVars (sql: string): string { - const escQuote = (d: any | any[]): string => { - if (d == null) { - return 'NULL' - } - if (Array.isArray(d)) { - return 'ARRAY[' + d.map(escQuote).join(',') + ']' - } - switch (typeof d) { - case 'number': - if (isNaN(d) || !isFinite(d)) { - throw new Error('Invalid number value') - } - return d.toString() - case 'boolean': - return d ? 'TRUE' : 'FALSE' - case 'string': - return `'${d.replace(/'/g, "''")}'` - default: - throw new Error(`Unsupported value type: ${typeof d}`) - } - } - return sql.replaceAll(/(\$\d+)/g, (_, v) => { - return escQuote(this.getValues()[parseInt(v.substring(1)) - 1] ?? v) - }) - } -} - -abstract class PostgresAdapterBase implements DbAdapter { - protected readonly _helper: DBCollectionHelper - protected readonly tableFields = new Map() - - constructor ( - protected readonly client: DBClient, - - protected readonly mgr: ConnectionMgr, - protected readonly refClient: { - url: () => string - close: () => void - }, - protected readonly workspaceId: WorkspaceUuid, - protected readonly hierarchy: Hierarchy, - protected readonly modelDb: ModelDb, - readonly mgrId: string - ) { - this._helper = new DBCollectionHelper(this.client, this.workspaceId) - } - - reserveContext (id: string): () => void { - this.mgr.getConnection(id, this.mgrId, true) - return () => { - this.mgr.release(id) // We need to release first - } - } - - async traverse( - _domain: Domain, - query: DocumentQuery, - options?: Pick, 'sort' | 'limit' | 'projection'> - ): Promise> { - const schema = getSchema(_domain) - const client = await this.client.reserve() - - const tdomain = translateDomain(_domain) - - const vars = new ValuesVariables() - const sqlChunks: string[] = [`SELECT * FROM ${tdomain}`] - sqlChunks.push(`WHERE ${this.buildRawQuery(vars, tdomain, query, options)}`) - if (options?.sort !== undefined) { - sqlChunks.push(this.buildRawOrder(tdomain, options.sort)) - } - if (options?.limit !== undefined) { - sqlChunks.push(`LIMIT ${options.limit}`) - } - const finalSql: string = sqlChunks.join(' ') - - const rawClient = client.raw() - - const cursor: AsyncGenerator = createCursorGenerator(rawClient, finalSql, vars.getValues(), schema) - return { - next: async (count: number): Promise => { - const result = await cursor.next() - if (result.done === true || result.value.length === 0) { - return null - } - return result.value as T[] - }, - close: async () => { - await cursor.return([]) - client.release() - } - } - } - - helper (): DomainHelperOperations { - return this._helper - } - - on?: ((handler: DbAdapterHandler) => void) | undefined - - abstract init ( - ctx: MeasureContext, - contextVars: Record, - domains?: string[], - excludeDomains?: string[] - ): Promise - - async close (): Promise { - this.mgr.close(this.mgrId) - this.refClient.close() - } - - async rawFindAll(_domain: Domain, query: DocumentQuery, options?: FindOptions): Promise { - const domain = translateDomain(_domain) - const vars = new ValuesVariables() - const select = `SELECT ${this.getProjection(vars, domain, options?.projection, [], options?.associations)} FROM ${domain}` - const sqlChunks: string[] = [] - sqlChunks.push(`WHERE ${this.buildRawQuery(vars, domain, query, options)}`) - if (options?.sort !== undefined) { - sqlChunks.push(this.buildRawOrder(domain, options.sort)) - } - if (options?.limit !== undefined) { - sqlChunks.push(`LIMIT ${options.limit}`) - } - const finalSql: string = [select, ...sqlChunks].join(' ') - const result: DBDoc[] = await this.mgr.retry(undefined, this.mgrId, (client) => - client.execute(finalSql, vars.getValues()) - ) - return result.map((p) => parseDocWithProjection(p, domain, options?.projection)) - } - - buildRawOrder(domain: string, sort: SortingQuery): string { - const res: string[] = [] - for (const key in sort) { - const val = sort[key] - if (val === undefined) { - continue - } - if (typeof val === 'number') { - res.push(`${this.transformKey(domain, core.class.Doc, key, false)} ${val === 1 ? 'ASC' : 'DESC'}`) - } else { - // todo handle custom sorting - } - } - if (res.length > 0) { - return `ORDER BY ${res.join(', ')}` - } else { - return '' - } - } - - buildRawQuery( - vars: ValuesVariables, - domain: string, - query: DocumentQuery, - options?: FindOptions - ): string { - const res: string[] = [] - res.push(`"workspaceId" = ${vars.add(this.workspaceId, '::uuid')}`) - for (const key in query) { - const value = query[key] - const tkey = this.transformKey(domain, core.class.Doc, key, false) - const translated = this.translateQueryValue(vars, tkey, value, 'common') - if (translated !== undefined) { - res.push(translated) - } - } - return res.join(' AND ') - } - - async rawUpdate( - domain: Domain, - query: DocumentQuery, - operations: DocumentUpdate - ): Promise { - const vars = new ValuesVariables() - const translatedQuery = this.buildRawQuery(vars, domain, query) - if ((operations as any).$set !== undefined) { - ;(operations as any) = { ...(operations as any).$set } - } - const isOps = isOperator(operations) - if ((operations as any)['%hash%'] == null) { - ;(operations as any)['%hash%'] = this.curHash() - } - const schemaFields = getSchemaAndFields(domain) - if (isOps) { - await this.mgr.write(undefined, this.mgrId, async (client) => { - const res = await client.execute( - `SELECT * FROM ${translateDomain(domain)} WHERE ${translatedQuery} FOR UPDATE`, - vars.getValues() - ) - const docs = res.map((p) => parseDoc(p, schemaFields.schema)) - for (const doc of docs) { - if (doc === undefined) continue - const prevAttachedTo = (doc as any).attachedTo - TxProcessor.applyUpdate(doc, operations) - ;(doc as any)['%hash%'] = this.curHash() - const converted = convertDoc(domain, doc, this.workspaceId, schemaFields) - const params = new ValuesVariables() - const updates: string[] = [] - const { extractedFields, remainingData } = parseUpdate(operations, schemaFields) - const newAttachedTo = (doc as any).attachedTo - if (Object.keys(extractedFields).length > 0) { - for (const key in extractedFields) { - const val = (extractedFields as any)[key] - if (key === 'attachedTo' && val === prevAttachedTo) continue - updates.push(`"${key}" = ${params.add(val)}`) - } - } else if (prevAttachedTo !== undefined && prevAttachedTo !== newAttachedTo) { - updates.push(`"attachedTo" = ${params.add(newAttachedTo)}`) - } - - if (Object.keys(remainingData).length > 0) { - updates.push(`data = ${params.add(converted.data, '::json')}`) - } - await client.execute( - `UPDATE ${translateDomain(domain)} - SET ${updates.join(', ')} - WHERE "workspaceId" = ${params.add(this.workspaceId, '::uuid')} - AND _id = ${params.add(doc._id, '::text')}`, - params.getValues() - ) - } - }) - } else { - await this.rawUpdateDoc(domain, query, operations, schemaFields) - } - } - - private async rawUpdateDoc( - domain: Domain, - query: DocumentQuery, - operations: DocumentUpdate, - schemaFields: SchemaAndFields - ): Promise { - const vars = new ValuesVariables() - const translatedQuery = this.buildRawQuery(vars, domain, query) - const updates: string[] = [] - const { extractedFields, remainingData } = parseUpdate(operations, schemaFields) - const { space, attachedTo, ...ops } = operations as any - for (const key in extractedFields) { - updates.push(`"${key}" = ${vars.add((extractedFields as any)[key])}`) - } - let from = 'data' - let dataUpdated = false - for (const key in remainingData) { - if (ops[key] === undefined) continue - const val = (remainingData as any)[key] - from = `jsonb_set(${from}, '{${key}}', coalesce(to_jsonb(${vars.add(val)}${inferType(val)}), 'null') , true)` - dataUpdated = true - } - if (dataUpdated) { - updates.push(`data = ${from}`) - } - await this.mgr.retry(undefined, this.mgrId, async (client) => { - await client.execute( - `UPDATE ${translateDomain(domain)} SET ${updates.join(', ')} WHERE ${translatedQuery};`, - vars.getValues() - ) - }) - } - - async rawDeleteMany(domain: Domain, query: DocumentQuery): Promise { - const vars = new ValuesVariables() - const translatedQuery = this.buildRawQuery(vars, domain, query) - await this.mgr.retry(undefined, this.mgrId, async (client) => { - await client.execute(`DELETE FROM ${translateDomain(domain)} WHERE ${translatedQuery}`, vars.getValues()) - }) - } - - findAll( - ctx: MeasureContext, - _class: Ref>, - query: DocumentQuery, - options?: ServerFindOptions - ): Promise> { - let fquery: any = '' - const vars = new ValuesVariables() - return ctx.with( - 'findAll', - {}, - async () => { - try { - const domain = translateDomain(this.hierarchy.getDomain(_class)) - const sqlChunks: string[] = [] - - const joins = this.buildJoins(_class, options) - // Add workspace name as $1 - - const projection = this.localizeProjection(_class, options?.projection ?? undefined) - - const select = `SELECT ${this.getProjection(vars, domain, projection, joins, options?.associations)} FROM ${domain}` - - const showArchived = shouldShowArchived(query, options) - const secJoin = this.addSecurity(_class, vars, query, showArchived, domain, ctx.contextData) - if (secJoin !== undefined) { - sqlChunks.push(secJoin) - } - if (joins.length > 0) { - sqlChunks.push(this.buildJoinString(vars, joins)) - } - sqlChunks.push(`WHERE ${this.buildQuery(vars, _class, domain, query, joins, options)}`) - - if (options?.sort !== undefined) { - sqlChunks.push(this.buildOrder(_class, domain, options.sort, joins)) - } - if (options?.limit !== undefined) { - sqlChunks.push(`LIMIT ${escape(options.limit)}`) - } - - return (await this.mgr.retry(ctx.id, this.mgrId, async (connection) => { - let total = options?.total === true ? 0 : -1 - if (options?.total === true) { - const pvars = new ValuesVariables() - const showArchived = shouldShowArchived(query, options) - const secJoin = this.addSecurity(_class, pvars, query, showArchived, domain, ctx.contextData) - const totalChunks: string[] = [] - if (secJoin !== undefined) { - totalChunks.push(secJoin) - } - const joins = this.buildJoin(_class, options?.lookup) - if (joins.length > 0) { - totalChunks.push(this.buildJoinString(pvars, joins)) - } - totalChunks.push(`WHERE ${this.buildQuery(pvars, _class, domain, query, joins, options)}`) - - const totalReq = `SELECT COUNT(${domain}._id) as count FROM ${domain}` - const totalSql = [totalReq, ...totalChunks].join(' ') - const totalResult = await connection.execute(totalSql, pvars.getValues()) - const parsed = Number.parseInt(totalResult[0].count) - total = Number.isNaN(parsed) ? 0 : parsed - } - - const finalSql: string = [select, ...sqlChunks].join(' ') - fquery = finalSql - - const result = await connection.execute(finalSql, vars.getValues()) - if (options?.lookup === undefined && options?.associations === undefined) { - return toFindResult( - result.map((p) => parseDocWithProjection(p, domain, projection)), - total - ) - } else { - const res = this.parseLookup(result, joins, projection, domain) - return toFindResult(res, total) - } - })) as FindResult - } catch (err) { - const sqlFull = vars.injectVars(fquery) - ctx.error('Error in findAll', { err, sql: fquery, sqlFull, query }) - throw err - } - }, - () => ({ - query, - psql: fquery - .replace(/\s+/g, ' ') - .replace(/(FROM|WHERE|ORDER BY|GROUP BY|LIMIT|OFFSET|LEFT JOIN|RIGHT JOIN|INNER JOIN|JOIN)/gi, '\n$1') - .split('\n'), - sql: vars.injectVars(fquery) - }) - ) - } - - private localizeProjection( - _class: Ref>, - projection: Projection | undefined - ): Projection | undefined { - if (projection === undefined) return - - const res: Projection = {} - if (!this.hierarchy.isMixin(_class)) { - for (const key in projection) { - ;(res as any)[escape(key)] = escape(projection[key]) - } - return res - } - - for (const key in projection) { - if (key.includes('.')) { - ;(res as any)[escape(key)] = escape(projection[key]) - } else { - try { - const attr = this.hierarchy.findAttribute(_class, key) - if (attr !== undefined && this.hierarchy.isMixin(attr.attributeOf)) { - const newKey = `${attr.attributeOf}.${attr.name}` as keyof Projection - res[newKey] = escape(projection[key]) - } else { - ;(res as any)[escape(key)] = escape(projection[key]) - } - } catch (err: any) { - // ignore, if - } - } - } - - return res - } - - private buildJoins(_class: Ref>, options: ServerFindOptions | undefined): JoinProps[] { - const joins = this.buildJoin(_class, options?.lookup) - return joins - } - - addSecurity( - _class: Ref>, - vars: ValuesVariables, - query: DocumentQuery, - showArchived: boolean, - domain: string, - sessionContext: SessionData - ): string | undefined { - if (sessionContext !== undefined && sessionContext.isTriggerCtx !== true) { - if (sessionContext.account?.role !== AccountRole.Admin && sessionContext.account !== undefined) { - const acc = sessionContext.account - if (acc.role === AccountRole.DocGuest || acc.uuid === systemAccountUuid) { - return - } - if (query.space === acc.uuid) return // TODO: was it for private spaces? If so, need to fix it as they are not identified by acc.uuid now - if (domain === DOMAIN_SPACE && isOwner(acc) && showArchived) return - const key = domain === DOMAIN_SPACE ? '_id' : domain === DOMAIN_TX ? '"objectSpace"' : 'space' - const privateCheck = domain === DOMAIN_SPACE ? ' OR sec.private = false' : '' - const archivedCheck = showArchived ? '' : ' AND sec.archived = false' - const q = `(sec._id = '${core.space.Space}' OR sec."_class" = '${core.class.SystemSpace}' OR sec.members @> '{"${acc.uuid}"}'${privateCheck})${archivedCheck}` - const res = `INNER JOIN ${translateDomain(DOMAIN_SPACE)} AS sec ON sec._id = ${domain}.${key} AND sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')}` - - const collabSec = getClassCollaborators(this.modelDb, this.hierarchy, _class) - if (collabSec?.provideSecurity === true && [AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(acc.role)) { - const collab = ` INNER JOIN ${translateDomain(DOMAIN_COLLABORATOR)} AS collab_sec ON collab_sec.collaborator = '${acc.uuid}' AND collab_sec."attachedTo" = ${domain}._id AND collab_sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} OR ${q}` - return res + collab - } - return `${res} AND ${q}` - } - } - } - - private parseLookup( - rows: any[], - joins: JoinProps[], - projection: Projection | undefined, - domain: string - ): WithLookup[] { - const map = new Map, WithLookup>() - const modelJoins: JoinProps[] = [] - const reverseJoins: JoinProps[] = [] - const simpleJoins: JoinProps[] = [] - for (const join of joins) { - if (join.table === DOMAIN_MODEL) { - modelJoins.push(join) - } else if (join.isReverse) { - reverseJoins.push(join) - } else if (join.path !== '') { - simpleJoins.push(join) - } - } - for (const row of rows) { - /* eslint-disable @typescript-eslint/consistent-type-assertions */ - let doc: WithLookup = map.get(row._id) ?? ({ _id: row._id, $lookup: {}, $associations: {} } as WithLookup) - const associations: Record = doc.$associations as Record - const lookup: Record = doc.$lookup as Record - let joinIndex: number | undefined - let skip = false - try { - const schema = getSchema(domain) - for (const column in row) { - if (column.startsWith('reverse_lookup_')) { - if (row[column] != null) { - const join = reverseJoins.find((j) => j.toAlias.toLowerCase() === column) - if (join === undefined) { - continue - } - const res = this.getLookupValue(join.path, lookup, false) - if (res === undefined) continue - const { obj, key } = res - - const parsed = row[column].map((p: any) => parseDoc(p, schema)) - obj[key] = parsed - } - } else if (column.startsWith('lookup_')) { - const keys = column.split('_') - let key = keys[keys.length - 1] - if (keys[keys.length - 2] === '') { - key = '_' + key - } - - if (key === 'workspaceId') { - continue - } - - if (key === '_id') { - joinIndex = joinIndex === undefined ? 0 : ++joinIndex - if (row[column] === null) { - skip = true - continue - } - skip = false - } - - if (skip) { - continue - } - - const join = simpleJoins[joinIndex ?? 0] - if (join === undefined) { - continue - } - const res = this.getLookupValue(join.path, lookup) - if (res === undefined) continue - const { obj, key: p } = res - - if (key === 'data') { - obj[p] = { ...obj[p], ...row[column] } - } else { - if (key === 'createdOn' || key === 'modifiedOn') { - const val = Number.parseInt(row[column]) - obj[p][key] = Number.isNaN(val) ? null : val - } else if (key === '%hash%') { - continue - } else if (key === 'attachedTo' && row[column] === 'NULL') { - continue - } else { - obj[p][key] = row[column] === 'NULL' ? null : row[column] - } - } - } else if (column.startsWith('assoc_')) { - if (row[column] == null) continue - const keys = column.split('_') - const key = keys[keys.length - 1] - const associationDomain = keys[1] - const associationSchema = getSchema(associationDomain) - const parsed = row[column].map((p: any) => parseDoc(p, associationSchema)) - associations[key] = parsed - } else { - joinIndex = undefined - if (!map.has(row._id)) { - if (column === 'workspaceId') { - continue - } - if (column === 'data') { - let data = row[column] - data = filterProjection(data, projection) - doc = { ...doc, ...data } - } else { - if (column === 'createdOn' || column === 'modifiedOn') { - const val = Number.parseInt(row[column]) - ;(doc as any)[column] = Number.isNaN(val) ? null : val - } else if (column === '%hash%') { - // Ignore - } else { - ;(doc as any)[column] = row[column] === 'NULL' ? null : row[column] - } - } - } - } - } - } catch (err) { - console.log(err) - throw err - } - for (const modelJoin of modelJoins) { - const res = this.getLookupValue(modelJoin.path, lookup) - if (res === undefined) continue - const { obj, key } = res - const val = this.getModelLookupValue(doc, modelJoin, [...simpleJoins, ...modelJoins]) - if (val !== undefined && modelJoin.toClass !== undefined) { - const res = this.modelDb.findAllSync(modelJoin.toClass, { - [modelJoin.toField]: val - }) - obj[key] = modelJoin.isReverse ? res : res[0] - } - } - map.set(row._id, doc) - } - return Array.from(map.values()) - } - - private getLookupValue ( - fullPath: string, - obj: Record, - shouldCreate: boolean = true - ): - | { - obj: any - key: string - } - | undefined { - const path = fullPath.split('.') - for (let i = 0; i < path.length; i++) { - const p = path[i] - if (i > 0) { - if (obj.$lookup === undefined) { - obj.$lookup = {} - } - obj = obj.$lookup - } - - if (obj[p] === undefined) { - if (!shouldCreate && i < path.length - 1) { - return - } else { - obj[p] = {} - } - } - if (i === path.length - 1) { - return { obj, key: p } - } - obj = obj[p] - } - } - - private getModelLookupValue(doc: WithLookup, join: JoinProps, otherJoins: JoinProps[]): any { - if (join.fromAlias.startsWith('lookup_')) { - const other = otherJoins.find((j) => j.toAlias === join.fromAlias) - if (other !== undefined) { - const val = this.getLookupValue(other.path, doc.$lookup ?? {}) - if (val !== undefined) { - const data = val.obj[val.key] - return data[join.fromField] - } - } - } else { - return (doc as any)[join.fromField] - } - } - - private buildJoinString (vars: ValuesVariables, value: JoinProps[]): string { - const res: string[] = [] - const wsId = vars.add(this.workspaceId, '::uuid') - for (const val of value) { - if (val.isReverse) continue - if (val.table === DOMAIN_MODEL) continue - res.push( - `LEFT JOIN ${val.table} AS ${val.toAlias} ON ${val.fromAlias}.${val.fromField} = ${val.toAlias}."${val.toField}" AND ${val.toAlias}."workspaceId" = ${wsId}` - ) - if (val.classes !== undefined) { - if (val.classes.length === 1) { - res.push(`AND ${val.toAlias}._class = ${vars.add(val.classes[0], '::text')}`) - } else { - res.push(`AND ${val.toAlias}._class = ANY (${vars.addArray(val.classes, '::text[]')})`) - } - } - } - return res.join(' ') - } - - private buildJoin(clazz: Ref>, lookup: Lookup | undefined): JoinProps[] { - const res: JoinProps[] = [] - if (lookup !== undefined) { - this.buildJoinValue(clazz, lookup, res) - } - return res - } - - private isArrayLookup (_class: Ref>, key: string): boolean { - const attr = this.hierarchy.findAttribute(_class, key) - if (attr === undefined) return false - if (attr.type._class === core.class.ArrOf) return true - return false - } - - private buildJoinValue( - clazz: Ref>, - lookup: Lookup, - res: JoinProps[], - parentKey?: string, - parentAlias?: string - ): void { - const baseDomain = parentAlias ?? translateDomain(this.hierarchy.getDomain(clazz)) - for (const _key in lookup) { - if (_key === '_id') { - this.getReverseLookupValue(baseDomain, lookup, res, parentKey) - continue - } - const value = (lookup as any)[_key] - const _class = Array.isArray(value) ? value[0] : value - const nested = Array.isArray(value) ? value[1] : undefined - const domain = translateDomain(this.hierarchy.getDomain(_class)) - const key = escape(_key) - if (this.isArrayLookup(clazz, key)) { - this.getArrayLookup(baseDomain, key, _class, res, domain, parentKey) - continue - } - const tkey = domain === DOMAIN_MODEL ? key : this.transformKey(baseDomain, clazz, key) - const as = `lookup_${domain}_${parentKey !== undefined ? parentKey + '_lookup_' + key : key}` - res.push({ - isReverse: false, - table: domain, - path: parentKey !== undefined ? `${parentKey}.${key}` : key, - toAlias: as, - toField: '_id', - fromField: tkey, - fromAlias: baseDomain, - toClass: _class - }) - if (nested !== undefined) { - this.buildJoinValue(_class, nested, res, key, as) - } - } - } - - private getArrayLookup ( - parentDomain: string, - key: string, - _class: Ref>, - result: JoinProps[], - domain: string, - parent?: string - ): void { - const desc = this.hierarchy - .getDescendants(this.hierarchy.getBaseClass(_class)) - .filter((it) => !this.hierarchy.isMixin(it)) - const as = `reverse_lookup_${domain}_${parent !== undefined ? parent + '_lookup_' + key : key}` - const from = isDataField(domain, key) - ? `ANY(SELECT jsonb_array_elements_text(${parentDomain}.data->'${key}'))` - : `ANY(${parentDomain}."${key}")` - result.push({ - isReverse: true, - table: domain, - toAlias: as, - toField: '_id', - classes: desc, - path: parent !== undefined ? `${parent}.${key}` : key, - fromAlias: '', - toClass: _class, - fromField: from - }) - } - - private getReverseLookupValue ( - parentDomain: string, - lookup: ReverseLookups, - result: JoinProps[], - parent?: string - ): void { - const lid = lookup?._id ?? {} - for (const key in lid) { - const value = lid[key] - - let _class: Ref> - let attr = 'attachedTo' - - if (Array.isArray(value)) { - _class = value[0] - attr = value[1] - } else { - _class = value - } - const domain = translateDomain(this.hierarchy.getDomain(_class)) - const desc = this.hierarchy - .getDescendants(this.hierarchy.getBaseClass(_class)) - .filter((it) => !this.hierarchy.isMixin(it)) - const as = `reverse_lookup_${domain}_${parent !== undefined ? parent + '_lookup_' + key : key}` - result.push({ - isReverse: true, - table: domain, - toAlias: as, - toField: attr, - classes: desc, - path: parent !== undefined ? `${parent}.${key}` : key, - fromAlias: parentDomain, - toClass: _class, - fromField: '_id' - }) - } - } - - private buildOrder( - _class: Ref>, - baseDomain: string, - sort: SortingQuery, - joins: JoinProps[] - ): string { - const res: string[] = [] - for (const _key in sort) { - const val = sort[_key] - if (val === undefined) { - continue - } - if (typeof val === 'number') { - const attr = this.hierarchy.findAttribute(_class, _key) - const key = escape(_key) - if (attr !== undefined && NumericTypes.includes(attr.type._class)) { - res.push(`(${this.getKey(_class, baseDomain, key, joins)})::numeric ${val === 1 ? 'ASC' : 'DESC'}`) - } else { - res.push(`${this.getKey(_class, baseDomain, key, joins)} ${val === 1 ? 'ASC' : 'DESC'}`) - } - } else { - // todo handle custom sorting - } - } - if (res.length > 0) { - return `ORDER BY ${res.join(', ')}` - } else { - return '' - } - } - - private buildQuery( - vars: ValuesVariables, - _class: Ref>, - baseDomain: string, - _query: DocumentQuery, - joins: JoinProps[], - options?: ServerFindOptions - ): string { - const res: string[] = [] - const query = { ..._query } - res.push(`${baseDomain}."workspaceId" = ${vars.add(this.workspaceId, '::uuid')}`) - if (options?.skipClass !== true) { - query._class = this.fillClass(_class, query) as any - } - for (const _key in query) { - if (options?.skipSpace === true && _key === 'space') { - continue - } - if (options?.skipClass === true && _key === '_class') { - continue - } - const value = query[_key] - if (value === undefined) continue - const key = escape(_key) - const valueType = this.getValueType(_class, key) - const tkey = this.getKey(_class, baseDomain, key, joins, valueType === 'dataArray') - const translated = this.translateQueryValue(vars, tkey, value, valueType) - if (translated !== undefined) { - res.push(translated) - } - } - return res.join(' AND ') - } - - private getValueType(_class: Ref>, key: string): ValueType { - const splitted = key.split('.') - const mixinOrKey = splitted[0] - const domain = this.hierarchy.getDomain(_class) - if (this.hierarchy.isMixin(mixinOrKey as Ref>)) { - key = splitted.slice(1).join('.') - const attr = this.hierarchy.findAttribute(mixinOrKey as Ref>, key) - if (attr !== undefined && attr.type._class === core.class.ArrOf) { - return isDataField(domain, key) ? 'dataArray' : 'array' - } - return 'common' - } else { - const attr = this.hierarchy.findAttribute(_class, key) - if (attr !== undefined && attr.type._class === core.class.ArrOf) { - return isDataField(domain, key) ? 'dataArray' : 'array' - } - return 'common' - } - } - - private fillClass( - _class: Ref>, - query: DocumentQuery - ): ObjQueryType | undefined { - let value: any = query._class - const baseClass = this.hierarchy.getBaseClass(_class) - if (baseClass !== core.class.Doc) { - const classes = this.hierarchy.getDescendants(baseClass).filter((it) => !this.hierarchy.isMixin(it)) - - // Only replace if not specified - if (value === undefined) { - value = { $in: classes } - } else if (typeof value === 'string') { - if (!classes.includes(value as Ref>)) { - value = classes.length === 1 ? classes[0] : { $in: classes } - } - } else if (typeof value === 'object' && value !== null) { - let descendants: Ref>[] = classes - - if (Array.isArray(value.$in)) { - const classesIds = new Set(classes) - descendants = value.$in.filter((c: Ref>) => classesIds.has(c)) - } - - if (value != null && Array.isArray(value.$nin)) { - const excludedClassesIds = new Set>>(value.$nin) - descendants = descendants.filter((c) => !excludedClassesIds.has(c)) - } - - if (value.$ne != null) { - descendants = descendants.filter((c) => c !== value?.$ne) - } - - const desc = descendants.filter((it: any) => !this.hierarchy.isMixin(it as Ref>)) - value = desc.length === 1 ? desc[0] : { $in: desc } - } - - if (baseClass !== _class) { - // Add an mixin to be exists flag - ;(query as any)[_class] = { $exists: true } - } - } else { - // No need to pass _class in case of fixed domain search. - return undefined - } - if (value?.$in?.length === 1 && value?.$nin === undefined) { - value = value.$in[0] - } - return value - } - - private getKey( - _class: Ref>, - baseDomain: string, - key: string, - joins: JoinProps[], - isDataArray: boolean = false - ): string { - if (key.startsWith('$lookup')) { - return this.transformLookupKey(baseDomain, key, joins, isDataArray) - } - return `${baseDomain}.${this.transformKey(baseDomain, _class, key, isDataArray)}` - } - - private transformLookupKey (domain: string, key: string, joins: JoinProps[], isDataArray: boolean = false): string { - const arr = key.split('.').filter((p) => p !== '$lookup') - const tKey = arr.pop() ?? '' - const path = arr.join('.') - const join = joins.find((p) => p.path === path) - if (join === undefined) { - throw new Error(`Can't fined join for path: ${path}`) - } - if (join.isReverse) { - return `${join.toAlias}->'${tKey}'` - } - if (isDataField(domain, tKey)) { - if (isDataArray) { - return `${join.toAlias}."data"->'${tKey}'` - } - return `${join.toAlias}."data"#>>'{${tKey}}'` - } - return `${join.toAlias}."${tKey}"` - } - - private transformKey( - domain: string, - _class: Ref>, - key: string, - isDataArray: boolean = false - ): string { - if (!isDataField(domain, key)) return `"${key}"` - const arr = key.split('.').filter((p) => p) - let tKey = '' - let isNestedField = false - - for (let i = 0; i < arr.length; i++) { - const element = arr[i] - if (element === '$lookup') { - tKey += arr[++i] + '_lookup' - } else if (this.hierarchy.isMixin(element as Ref>)) { - isNestedField = true - tKey += `${element}` - if (i !== arr.length - 1) { - tKey += "'->'" - } - } else { - tKey += arr[i] - if (i !== arr.length - 1) { - tKey += ',' - } - } - // Check if key is belong to mixin class, we need to add prefix. - tKey = this.checkMixinKey(tKey, _class, isDataArray) - } - - return isDataArray || isNestedField ? `data->'${tKey}'` : `data#>>'{${tKey}}'` - } - - private checkMixinKey(key: string, _class: Ref>, isDataArray: boolean): string { - if (!key.includes('.')) { - try { - const attr = this.hierarchy.findAttribute(_class, key) - if (attr !== undefined && this.hierarchy.isMixin(attr.attributeOf)) { - // It is mixin - if (isDataArray) { - key = `${attr.attributeOf}'->'${key}` - } else { - key = `${attr.attributeOf},${key}` - } - } - } catch (err: any) { - // ignore, if - } - } - return key - } - - private translateQueryValue (vars: ValuesVariables, tkey: string, value: any, type: ValueType): string | undefined { - const tkeyData = tkey.includes('data') && (tkey.includes('->') || tkey.includes('#>>')) - if (tkeyData && (Array.isArray(value) || (typeof value !== 'object' && typeof value !== 'string'))) { - value = Array.isArray(value) - ? value.map((it) => (it == null ? null : `${it}`)) - : value == null - ? null - : `${value}` - } - - if (value === null) { - return `${tkey} IS NULL` - } else if (typeof value === 'object' && !Array.isArray(value)) { - // we can have multiple criteria for one field - const res: string[] = [] - const nonOperator: Record = {} - for (const operator in value) { - let val = value[operator] - if (tkeyData && (Array.isArray(val) || (typeof val !== 'object' && typeof val !== 'string'))) { - val = Array.isArray(val) ? val.map((it) => (it == null ? null : `${it}`)) : val == null ? null : `${val}` - } - - let valType = inferType(val) - const { tlkey, arrowCount } = prepareJsonValue(tkey, valType) - if (arrowCount > 0 && valType === '::text') { - valType = '' - } - - switch (operator) { - case '$ne': - if (val == null) { - res.push(`${tlkey} IS NOT NULL`) - } else { - res.push(`${tlkey} != ${vars.add(val, valType)}`) - } - break - case '$gt': - res.push(`${tlkey} > ${vars.add(val, valType)}`) - break - case '$gte': - res.push(`${tlkey} >= ${vars.add(val, valType)}`) - break - case '$lt': - res.push(`${tlkey} < ${vars.add(val, valType)}`) - break - case '$lte': - res.push(`${tlkey} <= ${vars.add(val, valType)}`) - break - case '$in': - switch (type) { - case 'common': - if (Array.isArray(val) && val.includes(null)) { - const vv = vars.addArray(val, valType) - res.push(`(${tlkey} = ANY(${vv}) OR ${tkey} IS NULL)`) - } else { - if (val.length > 0) { - res.push(`${tlkey} = ANY(${vars.addArray(val, valType)})`) - } else { - res.push(`${tlkey} IN ('NULL')`) - } - } - break - case 'array': - { - const vv = vars.addArrayI(val, valType) - res.push(`${tkey} && ${vv}`) - } - break - case 'dataArray': - { - const vv = vars.addArrayI(val, valType) - res.push(`${tkey} ?| ${vv}`) - } - break - } - break - case '$nin': - if (Array.isArray(val) && val.includes(null)) { - res.push(`(${tlkey} != ALL(${vars.addArray(val, valType)}) AND ${tkey} IS NOT NULL)`) - } else if (Array.isArray(val) && val.length > 0) { - res.push(`${tlkey} != ALL(${vars.addArray(val, valType)})`) - } - break - case '$like': - res.push(`${tlkey} ILIKE ${vars.add(val, valType)}`) - break - case '$exists': - res.push(`${tlkey} IS ${val === true || val === 'true' ? 'NOT NULL' : 'NULL'}`) - break - case '$regex': - res.push(`${tlkey} SIMILAR TO ${vars.add(val, valType)}`) - break - case '$options': - break - case '$all': - if (arrowCount > 0) { - res.push(`${tkey} @> '${JSON.stringify(val)}'::jsonb`) - } else { - res.push(`${tkey} @> ${vars.addArray(val, valType)}`) - } - break - case '$size': { - let v = val - let op = '=' - if (typeof val === 'object') { - if (val.$gt !== undefined) { - v = val.$gt - op = '>' - } else if (val.$gte !== undefined) { - v = val.$gte - op = '>=' - } else if (val.$lt !== undefined) { - v = val.$lt - op = '<' - } else if (val.$lte !== undefined) { - v = val.$lte - op = '<=' - } - } - if (type === 'dataArray') { - res.push(`coalesce(jsonb_array_length(${tkey}), 0) ${op} ${vars.add(v, '::integer')}`) - } else { - res.push(`array_length(${tkey}, 1) ${op} ${vars.add(v, '::integer')}`) - } - break - } - default: - nonOperator[operator] = value[operator] - break - } - } - if (Object.keys(nonOperator).length > 0) { - const qkey = tkey.replace('#>>', '->').replace('{', '').replace('}', '') - res.push(`(${qkey} @> '${JSON.stringify(nonOperator)}' or ${qkey} @> '[${JSON.stringify(nonOperator)}]')`) - } - return res.length === 0 ? undefined : res.join(' AND ') - } - - let valType = inferType(value) - const { tlkey, arrowCount } = prepareJsonValue(tkey, valType) - if (arrowCount > 0 && valType === '::text') { - valType = '' - } - return type === 'common' - ? `${tlkey} = ${vars.add(value, valType)}` - : type === 'array' - ? `${tkey} @> '${typeof value === 'string' ? '{"' + escape(value) + '"}' : value}'` - : `${tkey} @> '${typeof value === 'string' ? '"' + escape(value) + '"' : value}'` - } - - private getReverseProjection (vars: ValuesVariables, join: JoinProps): string[] { - let classsesQuery = '' - if (join.classes !== undefined) { - if (join.classes.length === 1) { - classsesQuery = ` AND ${join.toAlias}._class = ${vars.add(join.classes[0])}` - } else { - classsesQuery = ` AND ${join.toAlias}._class = ANY (${vars.add(join.classes, '::text[]')})` - } - } - const wsId = vars.add(this.workspaceId, '::uuid') - return [ - `(SELECT jsonb_agg(${join.toAlias}.*) FROM ${join.table} AS ${join.toAlias} WHERE ${join.toAlias}."${join.toField}" = ${join.fromAlias}${join.fromAlias !== '' ? '.' : ''}${join.fromField} AND ${join.toAlias}."workspaceId" = ${wsId}${classsesQuery}) AS ${join.toAlias}` - ] - } - - private getProjectionsAliases (vars: ValuesVariables, join: JoinProps): string[] { - if (join.table === DOMAIN_MODEL) return [] - if (join.path === '') return [] - if (join.isReverse) return this.getReverseProjection(vars, join) - const fields = getDocFieldsByDomains(join.table) - const res: string[] = [] - for (const key of [...fields, 'data']) { - res.push(`${join.toAlias}."${key}" as "${join.toAlias}_${key}"`) - } - return res - } - - getAssociationsProjections (vars: ValuesVariables, baseDomain: string, associations: AssociationQuery[]): string[] { - const res: string[] = [] - for (const association of associations) { - const _id = escape(association[0]) - const assoc = this.modelDb.findObject(_id) - if (assoc === undefined) { - continue - } - const isReverse = association[1] === -1 - const _class = isReverse ? assoc.classA : assoc.classB - const domain = this.hierarchy.findDomain(_class) - if (domain === undefined) continue - const tagetDomain = translateDomain(domain) - const keyA = isReverse ? 'docB' : 'docA' - const keyB = isReverse ? 'docA' : 'docB' - const wsId = vars.add(this.workspaceId, '::uuid') - res.push( - `(SELECT jsonb_agg(assoc.*) - FROM ${tagetDomain} AS assoc - JOIN ${translateDomain(DOMAIN_RELATION)} as relation - ON relation."${keyB}" = assoc."_id" - AND relation."workspaceId" = ${wsId} - WHERE relation."${keyA}" = ${translateDomain(baseDomain)}."_id" - AND relation.association = '${_id}' - AND assoc."workspaceId" = ${wsId}) AS assoc_${tagetDomain}_${_id}` - ) - } - return res - } - - @withContext('get-domain-hash') - async getDomainHash (ctx: MeasureContext, domain: Domain): Promise { - return await calcHashHash(ctx, domain, this) - } - - curHash (): string { - return Date.now().toString(16) // Current hash value - } - - private getProjection( - vars: ValuesVariables, - baseDomain: string, - projection: Projection | undefined, - joins: JoinProps[], - associations: AssociationQuery[] | undefined - ): string | '*' { - if (projection === undefined && joins.length === 0 && associations === undefined) return `${baseDomain}.*` - const res: string[] = [] - let dataAdded = false - if (projection === undefined) { - res.push(`${baseDomain}.*`) - } else { - if (projection._id === undefined) { - res.push(`${baseDomain}."_id" AS "_id"`) - } - if (projection._class === undefined) { - res.push(`${baseDomain}."_class" AS "_class"`) - } - for (const _key in projection) { - const key = escape(_key) - if (isDataField(baseDomain, key)) { - if (!dataAdded) { - res.push(`${baseDomain}.data as data`) - dataAdded = true - } - } else { - res.push(`${baseDomain}."${key}" AS "${key}"`) - } - } - } - for (const join of joins) { - res.push(...this.getProjectionsAliases(vars, join)) - } - if (associations !== undefined) { - res.push(...this.getAssociationsProjections(vars, baseDomain, associations)) - } - return res.join(', ') - } - - async tx (ctx: MeasureContext, ...tx: Tx[]): Promise { - return [] - } - - stripHash(docs: T | T[]): T | T[] { - if (Array.isArray(docs)) { - docs.forEach((it) => { - if ('%hash%' in it) { - delete it['%hash%'] - } - return it - }) - } else if (typeof docs === 'object' && docs != null) { - if ('%hash%' in docs) { - delete docs['%hash%'] - } - } - return docs - } - - strimSize (str?: string): string { - if (str == null) { - return '' - } - const pos = str.indexOf('|') - if (pos > 0) { - return str.substring(0, pos) - } - return str - } - - find (_ctx: MeasureContext, domain: Domain): StorageIterator { - const ctx = _ctx.newChild('find', { domain }) - - let initialized: boolean = false - let client: DBClient - - const tdomain = translateDomain(domain) - const schema = getSchema(domain) - - const workspaceId = this.workspaceId - - function createBulk (projection: string, limit = 50000): AsyncGenerator { - const sql = ` - SELECT ${projection} - FROM ${tdomain} - WHERE "workspaceId" = '${workspaceId}' - ` - - return createCursorGenerator(client.raw(), sql, undefined, schema, limit) - } - let bulk: AsyncGenerator - - return { - next: async () => { - if (!initialized) { - if (client === undefined) { - client = await this.client.reserve() - } - initialized = true - bulk = createBulk('_id, "%hash%"') - } - - const docs = await ctx.with('next', {}, () => bulk.next()) - if (docs.done === true || docs.value.length === 0) { - return [] - } - const result: DocInfo[] = [] - for (const d of docs.value) { - result.push({ - id: d._id, - hash: this.strimSize((d as any)['%hash%']) - }) - } - return result - }, - close: async () => { - await bulk.return([]) // We need to close generator, just in case - client?.release() - ctx.end() - } - } - } - - rawFind (_ctx: MeasureContext, domain: Domain): RawFindIterator { - const ctx = _ctx.newChild('findRaw', { domain }) - - let initialized: boolean = false - let client: DBClient - - const tdomain = translateDomain(domain) - const schema = getSchema(domain) - - const workspaceId = this.workspaceId - - function createBulk (projection: string, limit = 1000): AsyncGenerator { - const sql = ` - SELECT ${projection} - FROM ${tdomain} - WHERE "workspaceId" = '${workspaceId}' - ` - - return createCursorGenerator(client.raw(), sql, undefined, schema, limit) - } - let bulk: AsyncGenerator - - return { - find: async () => { - if (!initialized) { - if (client === undefined) { - client = await this.client.reserve() - } - initialized = true - bulk = createBulk('*') - } - - const docs = await ctx.with('next', {}, () => bulk.next()) - if (docs.done === true || docs.value.length === 0) { - return [] - } - const result: Doc[] = [] - result.push(...(this.stripHash(docs.value) as Doc[])) - return result - }, - close: async () => { - await bulk.return([]) // We need to close generator, just in case - client?.release() - ctx.end() - } - } - } - - load (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return ctx.with('load', { domain }, async () => { - if (docs.length === 0) { - return [] - } - - return await this.mgr.retry('', this.mgrId, async (client) => { - const res = await client.execute( - `SELECT * - FROM ${translateDomain(domain)} - WHERE "workspaceId" = $1::uuid - AND _id = ANY($2::text[])`, - [this.workspaceId, docs] - ) - return res.map((p) => parseDocWithProjection(p, domain)) - }) - }) - } - - upload (ctx: MeasureContext, domain: Domain, docs: Doc[], handleConflicts: boolean = true): Promise { - return ctx.with('upload', { domain }, async (ctx) => { - const schemaFields = getSchemaAndFields(domain) - const filedsWithData = [...schemaFields.fields, 'data'] - - const insertFields = filedsWithData.map((field) => `"${field}"`) - const onConflict = handleConflicts ? filedsWithData.map((field) => `"${field}" = EXCLUDED."${field}"`) : [] - - const insertStr = insertFields.join(', ') - const onConflictStr = onConflict.join(', ') - - try { - const tdomain = translateDomain(domain) - const batchSize = 200 - for (let i = 0; i < docs.length; i += batchSize) { - const part = docs.slice(i, i + batchSize) - const values = new ValuesVariables() - const vars: string[] = [] - const wsId = values.add(this.workspaceId, '::uuid') - for (const doc of part) { - if (!('%hash%' in doc) || doc['%hash%'] === '' || doc['%hash%'] == null) { - ;(doc as any)['%hash%'] = this.curHash() // We need to set current hash - } - const d = convertDoc(domain, doc, this.workspaceId, schemaFields) - const variables = [ - wsId, - ...schemaFields.fields.map((field) => values.add(d[field], `::${schemaFields.schema[field].type}`)), - values.add(d.data, '::json') - ] - vars.push(`(${variables.join(', ')})`) - } - - const vals = vars.join(',') - const query = `INSERT INTO ${tdomain} ("workspaceId", ${insertStr}) VALUES ${vals} ${ - handleConflicts ? `ON CONFLICT ("workspaceId", _id) DO UPDATE SET ${onConflictStr}` : '' - };` - await this.mgr.retry(ctx.id, this.mgrId, async (client) => await client.execute(query, values.getValues())) - } - } catch (err: any) { - ctx.error('failed to upload', { err }) - throw err - } - }) - } - - async clean (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - const tdomain = translateDomain(domain) - const batchSize = 2500 - const query = `DELETE FROM ${tdomain} WHERE "workspaceId" = $1 AND _id = ANY($2::text[])` - - for (let i = 0; i < docs.length; i += batchSize) { - const part = docs.slice(i, i + batchSize) - await ctx.with('clean', {}, () => { - const params = [this.workspaceId, part] - return this.mgr.retry(ctx.id, this.mgrId, (client) => client.execute(query, params)) - }) - } - } - - groupBy( - ctx: MeasureContext, - domain: Domain, - field: string, - query?: DocumentQuery

- ): Promise> { - const key = isDataField(domain, field) ? `data ->> '${field}'` : `"${field}"` - return ctx.with('groupBy', { domain }, async (ctx) => { - try { - const vars = new ValuesVariables() - const sqlChunks: string[] = [ - `SELECT ${key} as _${field}, Count(*) AS count`, - `FROM ${translateDomain(domain)}`, - `WHERE ${this.buildRawQuery(vars, domain, query ?? {})}`, - `GROUP BY _${field}` - ] - const finalSql = sqlChunks.join(' ') - return await this.mgr.retry(ctx.id, this.mgrId, async (connection) => { - const result = await connection.execute(finalSql, vars.getValues()) - return new Map( - result.map((r) => [r[`_${field.toLowerCase()}`], typeof r.count === 'string' ? parseInt(r.count) : r.count]) - ) - }) - } catch (err) { - ctx.error('Error while grouping by', { domain, field }) - throw err - } - }) - } - - @withContext('insert') - async insert (ctx: MeasureContext, domain: string, docs: Doc[]): Promise { - await this.upload(ctx, domain as Domain, docs, false) - return {} - } -} - -interface OperationBulk { - add: Doc[] - updates: TxUpdateDoc[] - - removes: TxRemoveDoc[] - - mixins: TxMixin[] -} - -const initRateLimit = new RateLimiter(1) - -export class PostgresAdapter extends PostgresAdapterBase { - async init ( - ctx: MeasureContext, - contextVars: Record, - domains?: string[], - excludeDomains?: string[] - ): Promise { - let resultDomains = [...(domains ?? this.hierarchy.domains()), 'kanban'] - if (excludeDomains !== undefined) { - resultDomains = resultDomains.filter((it) => !excludeDomains.includes(it)) - } - const url = this.refClient.url() - await initRateLimit.exec(async () => { - await createTables(ctx, this.client.raw(), url, resultDomains) - }) - this._helper.domains = new Set(resultDomains as Domain[]) - } - - private process (txes: Tx[]): OperationBulk { - const ops: OperationBulk = { - add: [], - mixins: [], - removes: [], - updates: [] - } - const updateGroup = new Map, TxUpdateDoc>() - for (const tx of txes) { - switch (tx._class) { - case core.class.TxCreateDoc: - ops.add.push(TxProcessor.createDoc2Doc(tx as TxCreateDoc)) - break - case core.class.TxUpdateDoc: { - const updateTx = tx as TxUpdateDoc - if (isOperator(updateTx.operations)) { - ops.updates.push(updateTx) - } else { - const current = updateGroup.get(updateTx.objectId) - if (current !== undefined) { - current.operations = { ...current.operations, ...updateTx.operations } - updateGroup.set(updateTx.objectId, current) - } else { - updateGroup.set(updateTx.objectId, updateTx) - } - } - break - } - case core.class.TxRemoveDoc: - ops.removes.push(tx as TxRemoveDoc) - break - case core.class.TxMixin: - ops.mixins.push(tx as TxMixin) - break - case core.class.TxApplyIf: - break - default: - console.error('Unknown/Unsupported operation:', tx._class, tx) - break - } - } - ops.updates.push(...updateGroup.values()) - return ops - } - - private async txMixin (ctx: MeasureContext, tx: TxMixin, schemaFields: SchemaAndFields): Promise { - await ctx.with('tx-mixin', { _class: tx.objectClass, mixin: tx.mixin }, async (ctx) => { - await this.mgr.write(ctx.id, this.mgrId, async (client) => { - const doc = await this.findDoc(ctx, client, tx.objectClass, tx.objectId, true) - if (doc === undefined) return - TxProcessor.updateMixin4Doc(doc, tx) - ;(doc as any)['%hash%'] = this.curHash() - const domain = this.hierarchy.getDomain(tx.objectClass) - const converted = convertDoc(domain, doc, this.workspaceId, schemaFields) - const { extractedFields } = parseUpdate(tx.attributes as Partial, schemaFields) - - const params = new ValuesVariables() - - const wsId = params.add(this.workspaceId, '::uuid') - const oId = params.add(tx.objectId, '::text') - const updates: string[] = [] - for (const key of new Set([...Object.keys(extractedFields), ...['modifiedOn', 'modifiedBy', '%hash%']])) { - const val = (doc as any)[key] - updates.push(`"${key}" = ${params.add(val, `::${schemaFields.schema[key].type}`)}`) - } - updates.push(`data = ${params.add(converted.data, '::json')}`) - await client.execute( - `UPDATE ${translateDomain(domain)} - SET ${updates.join(', ')} - WHERE "workspaceId" = ${wsId} AND _id = ${oId}`, - params.getValues() - ) - }) - }) - return {} - } - - async tx (ctx: MeasureContext, ...txes: Tx[]): Promise { - const result: TxResult[] = [] - - const h = this.hierarchy - const byDomain = groupByArray(txes, (it) => { - if (TxProcessor.isExtendsCUD(it._class)) { - return h.findDomain((it as TxCUD).objectClass) - } - return undefined - }) - - for (const [domain, txs] of byDomain) { - if (domain === undefined) { - continue - } - - const ops = this.process(txs) - - const domainFields = getSchemaAndFields(domain) - if (ops.add.length > 0) { - const res = await this.insert(ctx, domain, ops.add) - if (Object.keys(res).length > 0) { - result.push(res) - } - } - - if (ops.updates.length > 0) { - const res = await this.txUpdateDoc(ctx, domain, ops.updates, domainFields) - for (const r of res) { - if (Object.keys(r).length > 0) { - result.push(r) - } - } - } - // TODO: Optimize mixins - for (const mix of ops.mixins) { - const res = await this.txMixin(ctx, mix, domainFields) - if (Object.keys(res).length > 0) { - result.push(res) - } - } - if (ops.removes.length > 0) { - await this.clean( - ctx, - domain, - ops.removes.map((it) => it.objectId) - ) - } - } - - return result - } - - protected async txUpdateDoc ( - ctx: MeasureContext, - domain: Domain, - txes: TxUpdateDoc[], - schemaFields: SchemaAndFields - ): Promise { - const byOperator = groupByArray(txes, (it) => isOperator(it.operations)) - - const withOperator = byOperator.get(true) - const withoutOperator = byOperator.get(false) - - const result: TxResult[] = [] - const tdomain = translateDomain(domain) - for (const tx of withOperator ?? []) { - let doc: Doc | undefined - const ops: any = { '%hash%': this.curHash(), ...tx.operations } - result.push( - await ctx.with('tx-update-doc', { _class: tx.objectClass }, async (ctx) => { - await this.mgr.write(ctx.id, this.mgrId, async (client) => { - doc = await this.findDoc(ctx, client, tx.objectClass, tx.objectId, true) - if (doc === undefined) return {} - ops.modifiedBy = tx.modifiedBy - ops.modifiedOn = tx.modifiedOn - TxProcessor.applyUpdate(doc, ops) - ;(doc as any)['%hash%'] = this.curHash() - const converted = convertDoc(domain, doc, this.workspaceId, schemaFields) - const updates: string[] = [] - const params = new ValuesVariables() - - const { extractedFields, remainingData } = parseUpdate(ops, schemaFields) - - const wsId = params.add(this.workspaceId, '::uuid') - const oId = params.add(tx.objectId, '::text') - - for (const key of new Set([...Object.keys(extractedFields), ...['modifiedOn', 'modifiedBy', '%hash%']])) { - const val = (doc as any)[key] - updates.push(`"${key}" = ${params.add(val, `::${schemaFields.schema[key].type}`)}`) - } - if (Object.keys(remainingData).length > 0) { - updates.push(`data = ${params.add(converted.data, '::json')}`) - } - await client.execute( - `UPDATE ${tdomain} - SET ${updates.join(', ')} - WHERE "workspaceId" = ${wsId} - AND _id = ${oId}`, - params.getValues() - ) - }) - if (tx.retrieve === true && doc !== undefined) { - return { object: doc } - } - return {} - }) - ) - } - if ((withoutOperator ?? [])?.length > 0) { - result.push(...(await this.updateDoc(ctx, domain, withoutOperator ?? [], schemaFields))) - } - return result - } - - private updateDoc( - ctx: MeasureContext, - domain: Domain, - txes: TxUpdateDoc[], - schemaFields: SchemaAndFields - ): Promise { - return ctx.with('update jsonb_set', {}, async (_ctx) => { - const operations: { - objectClass: Ref> - objectId: Ref - updates: string[] - fields: string[] - params: any[] - retrieve: boolean - }[] = [] - - for (const tx of txes) { - const fields: string[] = ['modifiedBy', 'modifiedOn', '%hash%'] - const updates: string[] = ['"modifiedBy" = $2', '"modifiedOn" = $3', '"%hash%" = $4'] - const params: any[] = [tx.modifiedBy, tx.modifiedOn, this.curHash()] - let paramsIndex = params.length - const { extractedFields, remainingData } = parseUpdate(tx.operations, schemaFields) - const { space, attachedTo, ...ops } = tx.operations as any - for (const key in extractedFields) { - fields.push(key) - updates.push(`"${key}" = $${paramsIndex++}`) - params.push((extractedFields as any)[key]) - } - if (Object.keys(remainingData).length > 0) { - const jsonData: Record = {} - // const vals: string[] = [] - for (const key in remainingData) { - if (ops[key] === undefined) continue - const val = (remainingData as any)[key] - jsonData[key] = val - } - fields.push('data') - params.push(jsonData) - updates.push(`data = COALESCE(data || $${paramsIndex++}::jsonb)`) - } - operations.push({ - objectClass: tx.objectClass, - objectId: tx.objectId, - updates, - fields, - params, - retrieve: tx.retrieve ?? false - }) - } - const tdomain = translateDomain(domain) - const result: TxResult[] = [] - try { - const schema = getSchema(domain) - const groupedUpdates = groupByArray(operations, (it) => it.fields.join(',')) - for (const groupedOps of groupedUpdates.values()) { - for (let i = 0; i < groupedOps.length; i += 200) { - const part = groupedOps.slice(i, i + 200) - let idx = 1 - const indexes: string[] = [] - const data: any[] = [] - data.push(this.workspaceId) - for (const op of part) { - indexes.push( - `($${++idx}::${schema._id.type ?? 'text'}, ${op.fields.map((it) => (it === 'data' ? `$${++idx}::jsonb` : `$${++idx}::${schema[it].type ?? 'text'}`)).join(',')})` - ) - data.push(op.objectId) - data.push(...op.params) - } - const op = `UPDATE ${tdomain} SET ${part[0].fields.map((it) => (it === 'data' ? 'data = COALESCE(data || update_data._data)' : `"${it}" = update_data."_${it}"`)).join(', ')} - FROM (values ${indexes.join(',')}) AS update_data(__id, ${part[0].fields.map((it) => `"_${it}"`).join(',')}) - WHERE "workspaceId" = $1::uuid AND "_id" = update_data.__id` - - await this.mgr.retry(ctx.id, this.mgrId, (client) => - _ctx.with('bulk-update', {}, () => client.execute(op, data)) - ) - } - } - const toRetrieve = operations.filter((it) => it.retrieve) - if (toRetrieve.length > 0) { - await this.mgr.retry(ctx.id, this.mgrId, async (client) => { - for (const op of toRetrieve) { - const object = await this.findDoc(_ctx, client, op.objectClass, op.objectId) - result.push({ object }) - } - }) - } - } catch (err: any) { - ctx.error('failed to update docs', { err }) - } - return result - }) - } - - private findDoc ( - ctx: MeasureContext, - client: DBClient, - _class: Ref>, - _id: Ref, - forUpdate: boolean = false - ): Promise { - const domain = this.hierarchy.getDomain(_class) - return ctx.with('find-doc', { _class }, async () => { - const res = await client.execute( - `SELECT * FROM "${translateDomain(domain)}" WHERE "workspaceId" = $1::uuid AND _id = $2::text ${ - forUpdate ? ' FOR UPDATE' : '' - }`, - [this.workspaceId, _id] - ) - const dbDoc = res[0] - return dbDoc !== undefined ? parseDoc(dbDoc, getSchema(domain)) : undefined - }) - } -} - -class PostgresTxAdapter extends PostgresAdapterBase implements TxAdapter { - async init ( - ctx: MeasureContext, - contextVars: Record, - domains?: string[], - excludeDomains?: string[] - ): Promise { - const resultDomains = domains ?? [DOMAIN_TX, DOMAIN_MODEL_TX] - await initRateLimit.exec(async () => { - const url = this.refClient.url() - await createTables(ctx, this.client.raw(), url, resultDomains) - }) - this._helper.domains = new Set(resultDomains as Domain[]) - } - - override async tx (ctx: MeasureContext, ...tx: Tx[]): Promise { - if (tx.length === 0) { - return [] - } - try { - const modelTxes: Tx[] = [] - const baseTxes: Tx[] = [] - for (const _tx of tx) { - if (_tx.objectSpace === core.space.Model) { - modelTxes.push(_tx) - } else { - baseTxes.push(_tx) - } - } - if (modelTxes.length > 0) { - await this.insert(ctx, DOMAIN_MODEL_TX, modelTxes) - } - if (baseTxes.length > 0) { - await this.insert(ctx, DOMAIN_TX, baseTxes) - } - } catch (err) { - console.error(err) - } - return [] - } - - async getModel (ctx: MeasureContext): Promise { - const res: DBDoc[] = await this.mgr.retry(undefined, this.mgrId, (client) => { - const query = ` - SELECT * - FROM "${translateDomain(DOMAIN_MODEL_TX)}" - WHERE "workspaceId" = $1::uuid - ORDER BY "modifiedOn"::bigint ASC, _id::text ASC - ` - return client.execute(query, [this.workspaceId]) - }) - - const model = res.map((p) => parseDoc(p, getSchema(DOMAIN_MODEL_TX))) - return this.stripHash(model) as Tx[] - } -} -function prepareJsonValue (tkey: string, valType: string): { tlkey: string, arrowCount: number } { - if (valType === '::string') { - valType = '' // No need to add a string conversion - } - const arrowCount = (tkey.match(/->/g) ?? []).length - // We need to convert to type without array if pressent - let tlkey = arrowCount > 0 ? `(${tkey})${valType.replace('[]', '')}` : tkey - - if (arrowCount > 0) { - // We need to replace only the last -> to ->> - tlkey = arrowCount === 1 ? tlkey.replace('->', '->>') : tlkey.replace(/->(?!.*->)/, '->>') - } - return { tlkey, arrowCount } -} - -/** - * @public - */ -export async function createPostgresAdapter ( - ctx: MeasureContext, - hierarchy: Hierarchy, - url: string, - wsIds: WorkspaceIds, - modelDb: ModelDb -): Promise { - const client = getDBClient(url) - const connection = await client.getClient() - return new PostgresAdapter( - createDBClient(connection), - client.mgr, - client, - wsIds.uuid, - hierarchy, - modelDb, - 'default-' + wsIds.url - ) -} -/** - * @public - */ -export async function createPostgresTxAdapter ( - ctx: MeasureContext, - hierarchy: Hierarchy, - url: string, - wsIds: WorkspaceIds, - modelDb: ModelDb -): Promise { - const client = getDBClient(url) - const connection = await client.getClient() - - return new PostgresTxAdapter( - createDBClient(connection), - client.mgr, - client, - wsIds.uuid, - hierarchy, - modelDb, - 'tx' + wsIds.url - ) -} diff --git a/server/postgres/src/types.ts b/server/postgres/src/types.ts deleted file mode 100644 index ede3b253f2f..00000000000 --- a/server/postgres/src/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type ValueType = 'common' | 'array' | 'dataArray' diff --git a/server/postgres/src/utils.ts b/server/postgres/src/utils.ts deleted file mode 100644 index 551fe89138c..00000000000 --- a/server/postgres/src/utils.ts +++ /dev/null @@ -1,505 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type Account, - AccountRole, - type Class, - type Doc, - type DocumentUpdate, - type Domain, - type FieldIndexConfig, - type MeasureContext, - type MixinUpdate, - platformNow, - platformNowDiff, - type Projection, - type Ref, - systemAccountUuid, - type WorkspaceUuid -} from '@hcengineering/core' -import { type DomainHelperOperations } from '@hcengineering/server-core' -import type postgres from 'postgres' -import { type ParameterOrJSON } from 'postgres' -import { - addSchema, - type DataType, - getDocFieldsByDomains, - getIndex, - getSchema, - getSchemaAndFields, - type Schema, - type SchemaAndFields, - translateDomain -} from './schemas' -import { retryTxn, type DBClient } from '@hcengineering/postgres-base' - -const loadedDomains = new Set() - -let loadedTables = new Set() - -export const NumericTypes = [ - core.class.TypeNumber, - core.class.TypeTimestamp, - core.class.TypeDate, - core.class.Collection -] - -export async function createTables ( - ctx: MeasureContext, - client: postgres.Sql, - url: string, - domains: string[] -): Promise { - const filtered = domains.filter((d) => !loadedDomains.has(url + translateDomain(d))) - if (filtered.length === 0) { - return - } - const mapped = filtered.map((p) => translateDomain(p)) - const t = platformNow() - loadedTables = - loadedTables.size === 0 - ? new Set( - ( - await ctx.with('load-table', {}, () => - client.unsafe(` - SELECT table_name - FROM information_schema.tables - WHERE table_schema NOT IN ('pg_catalog', 'information_schema') - AND table_name NOT LIKE 'pg_%' - AND table_name NOT LIKE 'cluster_%' - AND table_name NOT LIKE 'kv_%' - AND table_name NOT LIKE 'node_%'`) - ) - ).map((it) => it.table_name) - ) - : loadedTables - console.log('load-table', platformNowDiff(t)) - - const domainsToLoad = mapped.filter((it) => loadedTables.has(it)) - if (domainsToLoad.length > 0) { - await ctx.with('load-schemas', {}, () => getTableSchema(client, domainsToLoad)) - } - const domainsToCreate: string[] = [] - for (const domain of mapped) { - if (!loadedTables.has(domain)) { - domainsToCreate.push(domain) - } else { - loadedDomains.add(url + domain) - } - } - - if (domainsToCreate.length > 0) { - await retryTxn(client, async (client) => { - for (const domain of domainsToCreate) { - await ctx.with('create-table', {}, () => createTable(client, domain)) - loadedDomains.add(url + domain) - } - }) - } -} - -async function getTableSchema (client: postgres.Sql, domains: string[]): Promise { - const res = await client.unsafe(`SELECT column_name::name, data_type::text, is_nullable::text, table_name::name - FROM information_schema.columns - WHERE table_name IN (${domains.map((it) => `'${it}'`).join(', ')}) and table_schema = 'public'::name - ORDER BY table_name::name, ordinal_position::int ASC;`) - - const schemas: Record = {} - for (const column of res) { - if (column.column_name === 'workspaceId' || column.column_name === 'data') { - continue - } - - const schema = schemas[column.table_name] ?? {} - schemas[column.table_name] = schema - - schema[column.column_name] = { - type: parseDataType(column.data_type), - notNull: column.is_nullable === 'NO', - index: false - } - } - for (const [domain, schema] of Object.entries(schemas)) { - addSchema(domain, schema) - } -} - -function parseDataType (type: string): DataType { - switch (type) { - case 'text': - return 'text' - case 'bigint': - return 'bigint' - case 'boolean': - return 'bool' - case 'ARRAY': - return 'text[]' - } - return 'text' -} - -async function createTable (client: postgres.Sql, domain: string): Promise { - const schema = getSchema(domain) - const fields: string[] = [] - for (const key in schema) { - const val = schema[key] - fields.push(`"${key}" ${val.type} ${val.notNull ? 'NOT NULL' : ''}`) - } - const colums = fields.join(', ') - const res = await client.unsafe(`CREATE TABLE IF NOT EXISTS ${domain} ( - "workspaceId" uuid NOT NULL, - ${colums}, - data JSONB NOT NULL, - PRIMARY KEY("workspaceId", _id) - )`) - if (res.count > 0) { - for (const key in schema) { - const val = schema[key] - if (val.index) { - await client.unsafe(` - CREATE INDEX ${domain}_${key} ON ${domain} ${getIndex(val)} ("${key}") - `) - } - fields.push(`"${key}" ${val.type} ${val.notNull ? 'NOT NULL' : ''}`) - } - } -} - -export function convertDoc ( - domain: string, - doc: T, - workspaceId: WorkspaceUuid, - schemaAndFields?: SchemaAndFields -): DBDoc { - const extractedFields: Doc & Record = { - _id: doc._id, - space: doc.space, - createdBy: doc.createdBy, - modifiedBy: doc.modifiedBy, - modifiedOn: doc.modifiedOn, - createdOn: doc.createdOn ?? doc.modifiedOn, - _class: doc._class, - '%hash%': (doc as any)['%hash%'] ?? Date.now().toString(16) - } - const remainingData: Partial = {} - - const extractedFieldsKeys = new Set(Object.keys(extractedFields)) - - schemaAndFields = schemaAndFields ?? getSchemaAndFields(domain) - - for (const key in doc) { - if (extractedFieldsKeys.has(key)) { - continue - } - if (schemaAndFields.domainFields.has(key)) { - extractedFields[key] = doc[key] - } else { - remainingData[key] = doc[key] - } - } - - // Check if some fields are missing - for (const [key, _type] of Object.entries(schemaAndFields.schema)) { - if (_type.notNull) { - if (!(key in doc) || (doc as any)[key] == null) { - // We missing required field, and we need to add a dummy value for it. - // Null value is not allowed - switch (_type.type) { - case 'bigint': - extractedFields[key] = 0 - break - case 'bool': - extractedFields[key] = false - break - case 'text': - extractedFields[key] = '' - break - case 'text[]': - extractedFields[key] = [] - break - } - } - } - } - - const res: any = { - ...extractedFields, - workspaceId, - data: remainingData - } - return res -} - -export function inferType (val: any): string { - if (typeof val === 'string') { - return '::text' - } - if (typeof val === 'number') { - return '::numeric' - } - if (typeof val === 'boolean') { - return '::boolean' - } - if (Array.isArray(val)) { - const type = inferType(val[0] ?? val[1]) - if (type !== '') { - return type + '[]' - } - } - if (typeof val === 'object') { - if (val instanceof Date) { - return '::text' - } - return '::jsonb' - } - return '' -} - -export function parseUpdate ( - ops: DocumentUpdate | MixinUpdate, - schemaFields: SchemaAndFields -): { - extractedFields: Partial - remainingData: Partial - } { - const extractedFields: Partial = {} - const remainingData: Partial = {} - - for (const key in ops) { - const val = (ops as any)[key] - if (key.startsWith('$')) { - for (const k in val) { - if (schemaFields.domainFields.has(k)) { - ;(extractedFields as any)[k] = val[key] - } else { - ;(remainingData as any)[k] = val[key] - } - } - } else { - if (schemaFields.domainFields.has(key)) { - ;(extractedFields as any)[key] = val - } else { - ;(remainingData as any)[key] = val - } - } - } - - return { - extractedFields, - remainingData - } -} - -export function escapeBackticks (str: string): string { - if (typeof str !== 'string') return str - return str.replaceAll("'", "''") -} - -export function isOwner (account: Account): boolean { - return account.role === AccountRole.Owner || account.uuid === systemAccountUuid -} - -export class DBCollectionHelper implements DomainHelperOperations { - constructor ( - protected readonly client: DBClient, - protected readonly workspaceId: WorkspaceUuid - ) {} - - async dropIndex (domain: Domain, name: string): Promise {} - - domains = new Set() - async create (domain: Domain): Promise {} - - async exists (domain: Domain): Promise { - // Always exists. We don't need to check for index existence - return true - } - - async listDomains (): Promise> { - return this.domains - } - - async createIndex (domain: Domain, value: string | FieldIndexConfig, options?: { name: string }): Promise {} - - async listIndexes (domain: Domain): Promise<{ name: string }[]> { - return [] - } - - async estimatedCount (domain: Domain): Promise { - // We should always return 0, since no controlled index stuff is required for postgres driver - return 0 - } -} - -export function decodeArray (value: string): string[] { - if (value === 'NULL') return [] - // Remove first and last character (the array brackets) - const inner = value.substring(1, value.length - 1) - const items = inner.split(',') - return items.map((item) => { - // Remove quotes at start/end if they exist - let result = item - if (result.startsWith('"')) { - result = result.substring(1) - } - if (result.endsWith('"')) { - result = result.substring(0, result.length - 1) - } - // Replace escaped quotes with regular quotes - let final = '' - for (let i = 0; i < result.length; i++) { - if (result[i] === '\\' && result[i + 1] === '"') { - final += '"' - i++ // Skip next char - } else { - final += result[i] - } - } - return final - }) -} - -export function convertArrayParams (parameters?: ParameterOrJSON[]): any[] | undefined { - if (parameters === undefined) return undefined - return parameters.map((param) => { - if (Array.isArray(param)) { - if (param.length === 0) return '{}' - const sanitized = param.map((item) => { - if (item === null) return 'NULL' - if (typeof item === 'string') return `"${item.replace(/"/g, '\\"')}"` - return String(item) - }) - return `{${sanitized.join(',')}}` - } - return param - }) -} - -export function filterProjection (data: any, projection: Projection | undefined): any { - if (projection === undefined) { - return data - } - for (const key in data) { - if (!Object.prototype.hasOwnProperty.call(projection, key) || (projection as any)[key] === 0) { - // check nested projections in case of object - let value = data[key] - if (typeof value === 'object' && !Array.isArray(value) && value != null) { - // We need to filter projection for nested objects - const innerP = Object.entries(projection as any) - .filter((it) => it[0].startsWith(key)) - .map((it) => [it[0].substring(key.length + 1), it[1]]) - if (innerP.length > 0) { - value = filterProjection(value, Object.fromEntries(innerP)) - data[key] = value - continue - } - } - - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete data[key] - } - } - return data -} - -export function parseDocWithProjection ( - doc: DBDoc, - domain: string, - projection?: Projection | undefined -): T { - const { workspaceId, data, '%hash%': hash, ...rest } = doc - const schema = getSchema(domain) - for (const key in rest) { - if ((rest as any)[key] === 'NULL' || (rest as any)[key] === null) { - if (key === 'attachedTo') { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete rest[key] - } else { - ;(rest as any)[key] = null - } - } else if (schema[key] !== undefined && schema[key].type === 'bigint') { - ;(rest as any)[key] = Number.parseInt((rest as any)[key]) - } else if (schema[key] !== undefined && schema[key].type === 'text[]' && typeof (rest as any)[key] === 'string') { - ;(rest as any)[key] = decodeArray((rest as any)[key]) - } - } - let resultData = data - if (projection !== undefined) { - resultData = filterProjection(data, projection) - } - const res = { - ...resultData, - ...rest - } as any as T - - return res -} - -export function parseDoc (doc: DBDoc, schema: Schema): T { - const { workspaceId, data, ...rest } = doc - for (const key in rest) { - if ((rest as any)[key] === 'NULL' || (rest as any)[key] === null) { - if (key === 'attachedTo') { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete rest[key] - } else { - ;(rest as any)[key] = null - } - } else if (schema[key] !== undefined && schema[key].type === 'bigint') { - ;(rest as any)[key] = Number.parseInt((rest as any)[key]) - } else if (schema[key] !== undefined && schema[key].type === 'text[]' && typeof (rest as any)[key] === 'string') { - ;(rest as any)[key] = decodeArray((rest as any)[key]) - } - } - const res = { - ...data, - ...rest - } as any as T - - return res -} - -export interface DBDoc extends Doc { - workspaceId: WorkspaceUuid - data: Record - [key: string]: any -} - -export function isDataField (domain: string, field: string): boolean { - return !getDocFieldsByDomains(domain).includes(field) -} - -export interface JoinProps { - table: string // table to join - path: string // _id.roles, attachedTo.attachedTo, space... - fromAlias: string - fromField: string - toAlias: string // alias for the table - toField: string // field to join on - isReverse: boolean - toClass?: Ref> - classes?: Ref>[] // filter by classes -} - -export function escape (str: T): T { - if (typeof str === 'string') { - // Remove all characters except a-z, A-Z, 0-9 and _ . - // Add cyrillic for support old custom attributes - // eslint-disable-next-line no-useless-escape - return str.replace(/[^a-zA-ZА-Яа-яЁё0-9_.:\-\$ ]/g, '') as T - } - return str -} diff --git a/server/postgres/tsconfig.json b/server/postgres/tsconfig.json deleted file mode 100644 index c6a877cf6c3..00000000000 --- a/server/postgres/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/rpc/.eslintrc.js b/server/rpc/.eslintrc.js deleted file mode 100644 index 72235dc2833..00000000000 --- a/server/rpc/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/rpc/.npmignore b/server/rpc/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/rpc/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/rpc/config/rig.json b/server/rpc/config/rig.json deleted file mode 100644 index 0110930f55e..00000000000 --- a/server/rpc/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/server/rpc/jest.config.js b/server/rpc/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/rpc/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/rpc/package.json b/server/rpc/package.json deleted file mode 100644 index 3a3a6fb49ac..00000000000 --- a/server/rpc/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "@hcengineering/rpc", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@types/node": "^22.15.29", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" - }, - "dependencies": { - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "msgpackr": "^1.11.2" - }, - "repository": "https://github.com/hcengineering/platform", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/server/rpc/src/index.ts b/server/rpc/src/index.ts deleted file mode 100644 index 17c08121183..00000000000 --- a/server/rpc/src/index.ts +++ /dev/null @@ -1,18 +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. -// - -export * from './rpc' -export * from './sliding' diff --git a/server/rpc/src/rpc.ts b/server/rpc/src/rpc.ts deleted file mode 100644 index d1aa70f22df..00000000000 --- a/server/rpc/src/rpc.ts +++ /dev/null @@ -1,216 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Account } from '@hcengineering/core' -import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import { Packr } from 'msgpackr' - -/** - * @public - */ -export type ReqId = string | number - -/** - * @public - */ -export interface Request

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

(request: any, binary: boolean): Request

{ - const result: Request

= this.protoDeserialize(request, binary) - if (typeof result.method !== 'string') { - throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})) - } - return result - } -} - -/** - * @public - * @param status - - * @param id - - * @returns - */ -export function fromStatus (status: Status, id?: ReqId): Response { - return { id, error: status } -} diff --git a/server/rpc/src/sliding.ts b/server/rpc/src/sliding.ts deleted file mode 100644 index 15ffbfa2758..00000000000 --- a/server/rpc/src/sliding.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { RateLimitInfo } from './rpc' - -export class SlidingWindowRateLimitter { - private readonly rateLimits = new Map< - string, - { - requests: number[] - rejectedRequests: number // Counter for rejected requests - resetTime: number - } - >() - - constructor ( - readonly rateLimitMax: number, - readonly rateLimitWindow: number, - readonly now: () => number = Date.now - ) { - this.rateLimitMax = rateLimitMax - this.rateLimitWindow = rateLimitWindow - } - - public checkRateLimit (groupId: string): RateLimitInfo { - const now = this.now() - const windowStart = now - this.rateLimitWindow - - let rateLimit = this.rateLimits.get(groupId) - if (rateLimit == null) { - rateLimit = { requests: [], resetTime: now + this.rateLimitWindow, rejectedRequests: 0 } - this.rateLimits.set(groupId, rateLimit) - } - - // Remove requests outside the current window - rateLimit.requests = rateLimit.requests.filter((time) => time > windowStart) - - // Reset rejected requests counter when window changes - if (rateLimit.requests.length === 0) { - rateLimit.rejectedRequests = 0 - } - - // Update reset time - rateLimit.resetTime = now + this.rateLimitWindow - - if (rateLimit.requests.length <= this.rateLimitMax) { - rateLimit.requests.push(now + (rateLimit.rejectedRequests > this.rateLimitMax * 2 ? this.rateLimitWindow * 5 : 0)) - } - - if (rateLimit.requests.length >= this.rateLimitMax) { - rateLimit.rejectedRequests++ - - // Find when the oldest request will exit the window - const nextAvailableTime = rateLimit.requests[0] + this.rateLimitWindow - - return { - remaining: 0, - limit: this.rateLimitMax, - current: rateLimit.requests.length, - reset: rateLimit.resetTime, - retryAfter: Math.max(1, nextAvailableTime - now + 1) - } - } - - return { - remaining: this.rateLimitMax - rateLimit.requests.length, - current: rateLimit.requests.length, - limit: this.rateLimitMax, - reset: rateLimit.resetTime - } - } - - // Add a reset method for testing purposes - public reset (): void { - this.rateLimits.clear() - } -} diff --git a/server/rpc/src/test/rateLimit.spec.ts b/server/rpc/src/test/rateLimit.spec.ts deleted file mode 100644 index cdafa573dba..00000000000 --- a/server/rpc/src/test/rateLimit.spec.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { SlidingWindowRateLimitter } from '../sliding' - -describe('SlidingWindowRateLimitter', () => { - let clock = 100000 - - beforeEach(() => { - // Mock Date.now to control time - clock = 100000 - }) - - afterEach(() => { - jest.restoreAllMocks() - }) - - it('should allow requests within the limit', () => { - const limiter = new SlidingWindowRateLimitter(5, 60000, () => clock) - - for (let i = 0; i < 5; i++) { - const result = limiter.checkRateLimit('user1') - expect(result.remaining).toBe(5 - i - 1) - expect(result.limit).toBe(5) - } - - // The next request should hit the limit - const result = limiter.checkRateLimit('user1') - expect(result.remaining).toBe(0) - expect(result.retryAfter).toBeDefined() - }) - - it('should reject requests beyond the limit', () => { - const limiter = new SlidingWindowRateLimitter(3, 60000, () => clock) - - // Use up the limit - limiter.checkRateLimit('user1') - limiter.checkRateLimit('user1') - limiter.checkRateLimit('user1') - - // This should be limited - const result = limiter.checkRateLimit('user1') - expect(result.remaining).toBe(0) - expect(result.retryAfter).toBeDefined() - }) - - it('should allow new requests as the window slides', () => { - const limiter = new SlidingWindowRateLimitter(2, 10000, () => clock) - - // Use up the limit - limiter.checkRateLimit('user1') - limiter.checkRateLimit('user1') - - // This should be limited - expect(limiter.checkRateLimit('user1').remaining).toBe(0) - - // Move time forward by 5 seconds (half the window) - clock += 5 * 1000 // 5 seconds - - // Should still have one request outside the current window - // and one within, so we can make one more request - const result = limiter.checkRateLimit('user1') - expect(result.remaining).toBe(0) // Now at limit again - - // Move time forward by full window - clock += 11 * 1000 // 1011 seconds - - // All previous requests should be outside the window - const newResult = limiter.checkRateLimit('user1') - expect(newResult.remaining).toBe(1) // One request used, one remaining - expect(limiter.checkRateLimit('user1').remaining).toBe(0) // Now at limit again - }) - - it('should handle different identifiers separately', () => { - const limiter = new SlidingWindowRateLimitter(2, 60000, () => clock) - - limiter.checkRateLimit('user1') - limiter.checkRateLimit('user1') - - // User1 should be at limit - expect(limiter.checkRateLimit('user1').remaining).toBe(0) - - // Different user should have separate limit - expect(limiter.checkRateLimit('user2').remaining).toBe(1) - expect(limiter.checkRateLimit('user2').remaining).toBe(0) - - // Both users should be at their limits - expect(limiter.checkRateLimit('user1').remaining).toBe(0) - expect(limiter.checkRateLimit('user2').remaining).toBe(0) - }) - - it('should handle sliding window correctly', () => { - const limiter = new SlidingWindowRateLimitter(10, 60000, () => clock) - - // Use up half the capacity - for (let i = 0; i < 5; i++) { - limiter.checkRateLimit('user1') - } - - // Move halfway through the window - clock += 30 * 1000 + 1 // 30 seconds - - // Make some more requests - for (let i = 0; i < 7; i++) { - const result = limiter.checkRateLimit('user1') - if (i < 5) { - expect(result.remaining).toBeGreaterThanOrEqual(0) - } else { - expect(result.remaining).toBe(0) - expect(result.retryAfter).toBeDefined() - break - } - } - }) - - it('check for ban', () => { - const limiter = new SlidingWindowRateLimitter(10, 10000, () => clock) - - for (let i = 0; i < 50; i++) { - limiter.checkRateLimit('user1') - } - - const r1 = limiter.checkRateLimit('user1') - expect(r1.remaining).toBe(0) - // Pass all window time. - clock += 10000 - - const r2 = limiter.checkRateLimit('user1') - expect(r2.remaining).toBe(9) - }) -}) diff --git a/server/rpc/tsconfig.json b/server/rpc/tsconfig.json deleted file mode 100644 index b5ae22f6e46..00000000000 --- a/server/rpc/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/s3/.eslintrc.js b/server/s3/.eslintrc.js deleted file mode 100644 index ce90fb9646f..00000000000 --- a/server/s3/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/s3/.npmignore b/server/s3/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/s3/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/s3/config/rig.json b/server/s3/config/rig.json deleted file mode 100644 index 78cc5a17334..00000000000 --- a/server/s3/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/server/s3/jest.config.js b/server/s3/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/s3/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/s3/package.json b/server/s3/package.json deleted file mode 100644 index 8ef7f52622b..00000000000 --- a/server/s3/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@hcengineering/s3", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "^0.7.10", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "@types/node": "^22.15.29", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" - }, - "dependencies": { - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "@hcengineering/storage": "^0.7.3", - "@aws-sdk/client-s3": "^3.738.0", - "@aws-sdk/s3-request-presigner": "^3.738.0", - "@aws-sdk/lib-storage": "^3.738.0", - "@smithy/node-http-handler": "^4.0.2" - } -} diff --git a/server/s3/src/__tests__/s3.test.ts b/server/s3/src/__tests__/s3.test.ts deleted file mode 100644 index 7e15cadf5e9..00000000000 --- a/server/s3/src/__tests__/s3.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { MeasureMetricsContext, type WorkspaceDataId, type WorkspaceUuid, generateId } from '@hcengineering/core' -import { objectsToArray, type StorageConfiguration } from '@hcengineering/server-core' -import { S3Service, processConfigFromEnv, type S3Config } from '..' - -describe('s3 operations', () => { - const config: StorageConfiguration = { default: 'minio', storages: [] } - const minioConfigVar = processConfigFromEnv(config) - if (minioConfigVar !== undefined || config.storages[0] === undefined) { - console.error('No S3 config env is configured:' + minioConfigVar) - it.skip('No S3 config env is configured', async () => {}) - return - } - const toolCtx = new MeasureMetricsContext('test', {}) - it('check root bucket', async () => { - jest.setTimeout(50000) - const minioService = new S3Service({ ...(config.storages[0] as S3Config), rootBucket: 'haiodo-test-bucket' }) - - let existingTestBuckets = await minioService.listBuckets(toolCtx) - // Delete old buckets - for (const b of existingTestBuckets) { - await b.delete() - } - - const genWorkspaceId1 = generateId() as unknown as WorkspaceDataId - const genWorkspaceId2 = generateId() as unknown as WorkspaceDataId - - expect(genWorkspaceId1).not.toEqual(genWorkspaceId2) - - const wsIds1 = { - uuid: genWorkspaceId1 as unknown as WorkspaceUuid, - dataId: genWorkspaceId1, - url: '' - } - const wsIds2 = { - uuid: genWorkspaceId2 as unknown as WorkspaceUuid, - dataId: genWorkspaceId2, - url: '' - } - await minioService.make(toolCtx, wsIds1) - await minioService.make(toolCtx, wsIds2) - - const v1 = generateId() - await minioService.put(toolCtx, wsIds1, 'obj1.txt', v1, 'text/plain') - await minioService.put(toolCtx, wsIds2, 'obj2.txt', v1, 'text/plain') - - const w1Objects = await objectsToArray(toolCtx, minioService, wsIds1) - expect(w1Objects.map((it) => it._id)).toEqual(['obj1.txt']) - - const w2Objects = await objectsToArray(toolCtx, minioService, wsIds2) - expect(w2Objects.map((it) => it._id)).toEqual(['obj2.txt']) - - await minioService.put(toolCtx, wsIds1, 'obj1.txt', 'obj1', 'text/plain') - await minioService.put(toolCtx, wsIds1, 'obj2.txt', 'obj2', 'text/plain') - - const w1Objects2 = await objectsToArray(toolCtx, minioService, wsIds1) - expect(w1Objects2.map((it) => it._id)).toEqual(['obj1.txt', 'obj2.txt']) - - const read = (await minioService.read(toolCtx, wsIds1, 'obj1.txt')) as unknown as Uint8Array[] - const data = Buffer.concat(read) - - expect('obj1').toEqual(data.toString()) - - existingTestBuckets = await minioService.listBuckets(toolCtx) - expect(existingTestBuckets.length).toEqual(2) - // Delete old buckets - for (const b of existingTestBuckets) { - await b.delete() - } - existingTestBuckets = await minioService.listBuckets(toolCtx) - expect(existingTestBuckets.length).toEqual(0) - }) -}) diff --git a/server/s3/src/index.ts b/server/s3/src/index.ts deleted file mode 100644 index 2fc098ad9d4..00000000000 --- a/server/s3/src/index.ts +++ /dev/null @@ -1,491 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { CopyObjectCommand, PutObjectCommand, S3 } from '@aws-sdk/client-s3' -import { Upload } from '@aws-sdk/lib-storage' -import { NodeHttpHandler } from '@smithy/node-http-handler' -import { Agent as HttpAgent } from 'http' -import { Agent as HttpsAgent } from 'https' - -import core, { - withContext, - type WorkspaceIds, - type Blob, - type MeasureContext, - type Ref, - type WorkspaceDataId, - type WorkspaceUuid -} from '@hcengineering/core' -import { getMetadata } from '@hcengineering/platform' -import serverCore, { - NoSuchKeyError, - getDataId, - type BlobStorageIterator, - type ListBlobResult, - type StorageAdapter, - type StorageConfig, - type StorageConfiguration, - type UploadedObjectInfo -} from '@hcengineering/server-core' -import { Readable } from 'stream' - -import { removeAllObjects, type BucketInfo } from '@hcengineering/storage' -import type { ReadableStream } from 'stream/web' - -export interface S3Config extends StorageConfig { - kind: 's3' - accessKey: string - secretKey: string - region?: string - - // If defined, all resources will be inside selected root bucket. - rootBucket?: string - - // A prefix string to be added to a bucketId in case rootBucket not used - bucketPrefix?: string - - // If not specified will be enabled - allowPresign?: string - // Expire time for presigned URIs - expireTime?: string -} - -export const CONFIG_KIND = 's3' - -/** - * @public - */ -export class S3Service implements StorageAdapter { - expireTime: number - client: S3 - constructor (readonly opt: S3Config) { - const endpoint = Number.isInteger(opt.port) ? `${opt.endpoint}:${opt.port}` : opt.endpoint - this.client = new S3({ - endpoint, - credentials: { - accessKeyId: opt.accessKey, - secretAccessKey: opt.secretKey - }, - region: opt.region ?? 'auto', - requestHandler: new NodeHttpHandler({ - connectionTimeout: 5000, - socketTimeout: 120000, - httpAgent: new HttpAgent({ maxSockets: 500, keepAlive: true }), - httpsAgent: new HttpsAgent({ maxSockets: 500, keepAlive: true }) - }) - }) - - this.expireTime = parseInt(this.opt.expireTime ?? '168') * 3600 // use 7 * 24 - hours as default value for expireF - } - - async initialize (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} - - /** - * @public - */ - getBucketId (wsIds: WorkspaceIds): string { - return this.opt.rootBucket ?? (this.opt.bucketPrefix ?? '') + getDataId(wsIds) - } - - getBucketFolder (wsIds: WorkspaceIds): string { - return getDataId(wsIds) - } - - async close (): Promise {} - - async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - try { - const result = await this.client.headBucket({ - Bucket: this.getBucketId(wsIds) - }) - return result.$metadata.httpStatusCode === 200 - } catch (err: any) { - if (err.name === '400') { - // No bucket exisrs - return false - } - } - // No API to check is bucket exists or not, so we need to call make and check if it already exists. - return false - } - - @withContext('make') - async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - try { - await this.client.createBucket({ - Bucket: this.getBucketId(wsIds) - }) - } catch (err: any) { - if (err.Code === 'BucketAlreadyOwnedByYou') { - return - } - ctx.error('error during create bucket', { err }) - } - } - - async listBuckets (ctx: MeasureContext): Promise { - try { - if (this.opt.rootBucket !== undefined) { - const info = new Map() - let token: string | undefined - - while (true) { - const res = await this.client.listObjectsV2({ - Bucket: this.opt.rootBucket, - Prefix: '', - Delimiter: '/', - ContinuationToken: token - }) - for (const data of res.CommonPrefixes ?? []) { - const wsDataId = data.Prefix?.split('/')?.[0] as WorkspaceDataId - const wsIds = { - uuid: wsDataId as unknown as WorkspaceUuid, - dataId: wsDataId, - url: '' - } - if (wsDataId !== undefined && !info.has(wsDataId)) { - info.set(wsDataId, { - name: wsDataId, - delete: async () => { - await this.delete(ctx, wsIds) - }, - list: async () => await this.listStream(ctx, wsIds) - }) - } - } - if (res.IsTruncated === true) { - token = res.NextContinuationToken - } else { - break - } - } - return Array.from(info.values()) - } else { - const productPostfix = this.getBucketFolder({ - uuid: '' as WorkspaceUuid, - dataId: '' as WorkspaceDataId, - url: '' - }) - const buckets = await this.client.listBuckets() - return (buckets.Buckets ?? []) - .filter((it) => it.Name !== undefined && it.Name.endsWith(productPostfix)) - .map((it) => { - let name = (it.Name ?? '') as WorkspaceDataId - name = name.slice(0, name.length - productPostfix.length) as WorkspaceDataId - const wsIds = { - uuid: name as unknown as WorkspaceUuid, - dataId: name, - url: '' - } - return { - name, - delete: async () => { - await this.delete(ctx, wsIds) - }, - list: async () => await this.listStream(ctx, wsIds) - } - }) - } - } catch (err: any) { - if (err.Code === 'NoSuchBucket') { - return [] - } - ctx.error('failed to list buckets', { rootBucket: this.opt.rootBucket }) - console.error(err) - return [] - } - } - - getDocumentKey (wsIds: WorkspaceIds, name: string): string { - return this.opt.rootBucket === undefined ? name : `${this.getBucketFolder(wsIds)}/${name}` - } - - @withContext('remove') - async remove (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]): Promise { - await this.client.deleteObjects({ - Bucket: this.getBucketId(wsIds), - Delete: { - Objects: objectNames.map((it) => ({ Key: this.getDocumentKey(wsIds, it) })) - } - }) - } - - @withContext('delete') - async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - try { - await removeAllObjects(ctx, this, wsIds) - } catch (err: any) { - ctx.error('failed to clean all objects', { error: err }) - } - if (this.opt.rootBucket === undefined) { - // We should also delete bucket - await this.client.deleteBucket({ - Bucket: this.getBucketId(wsIds) - }) - } - } - - stripPrefix (prefix: string | undefined, key: string): string { - if (prefix !== undefined && key.startsWith(prefix)) { - return key.slice(prefix.length) - } - return key - } - - rootPrefix (wsIds: WorkspaceIds): string | undefined { - return this.opt.rootBucket !== undefined ? this.getBucketFolder(wsIds) + '/' : undefined - } - - async copy (sourceId: WorkspaceIds, targetId: WorkspaceIds, objectName: string): Promise { - const copyOp = new CopyObjectCommand({ - Bucket: this.getBucketId(targetId), - Key: this.getDocumentKey(targetId, objectName), - CopySource: `${this.getBucketId(sourceId)}/${this.getDocumentKey(sourceId, objectName)}` - }) - await this.client.send(copyOp) - } - - @withContext('listStream') - async listStream (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - let hasMore = true - const buffer: ListBlobResult[] = [] - let token: string | undefined - - const rootPrefix = this.rootPrefix(wsIds) - return { - next: async (): Promise => { - try { - while (hasMore && buffer.length < 50) { - const res = await this.client.listObjectsV2({ - Bucket: this.getBucketId(wsIds), - Prefix: rootPrefix ?? '', - ContinuationToken: token - }) - if (res.IsTruncated === true) { - token = res.NextContinuationToken - } else { - hasMore = false - } - - for (const data of res.Contents ?? []) { - const _id = this.stripPrefix(rootPrefix, data.Key ?? '') - buffer.push({ - _id: _id as Ref, - _class: core.class.Blob, - etag: data.ETag ?? '', - size: data.Size ?? 0, - provider: this.opt.name, - space: core.space.Configuration, - modifiedBy: core.account.System, - modifiedOn: data.LastModified?.getTime() ?? 0 - }) - } - } - } catch (err: any) { - ctx.error('Failed to get list', { error: err, wsIds }) - } - return buffer.splice(0, 50) - }, - close: async () => {} - } - } - - @withContext('stat') - async stat (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - try { - const result = await this.client.headObject({ - Bucket: this.getBucketId(wsIds), - Key: this.getDocumentKey(wsIds, objectName) - }) - const rootPrefix = this.rootPrefix(wsIds) - return { - provider: '', - _class: core.class.Blob, - _id: this.stripPrefix(rootPrefix, objectName) as Ref, - contentType: result.ContentType ?? '', - size: result.ContentLength ?? 0, - etag: result.ETag ?? '', - space: core.space.Configuration, - modifiedBy: core.account.System, - modifiedOn: result.LastModified?.getTime() ?? 0, - version: result.VersionId ?? null - } - } catch (err: any) { - if (err?.$metadata?.httpStatusCode !== 404) { - ctx.warn('no object found', { error: err, objectName, wsIds }) - throw err - } - } - } - - @withContext('get') - async get (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - return await this.doGet(ctx, wsIds, objectName) - } - - async doGet (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string, range?: string): Promise { - try { - const res = await this.client.getObject({ - Bucket: this.getBucketId(wsIds), - Key: this.getDocumentKey(wsIds, objectName), - Range: range - }) - - const stream = res.Body?.transformToWebStream() - - if (stream !== undefined) { - return Readable.fromWeb(stream as ReadableStream) - } else { - const readable = new Readable() - readable._read = () => {} - readable.push(null) - return readable - } - } catch (err: any) { - // In case of error return undefined - throw new NoSuchKeyError(`uuid=${wsIds.uuid} dataId=${wsIds.dataId} missing ${objectName}`, err) - } - } - - @withContext('put') - put ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - stream: Readable | Buffer | string, - contentType: string, - size?: number - ): Promise { - if (size !== undefined && size < 1024 * 1024 * 5) { - return ctx.with( - 'simple-put', - {}, - async () => { - const cmd = new PutObjectCommand({ - Bucket: this.getBucketId(wsIds), - Key: this.getDocumentKey(wsIds, objectName), - ContentType: contentType, - ContentLength: size, - Body: stream - }) - const response = await this.client.send(cmd) - return { - etag: response.ETag ?? '', - versionId: response.VersionId ?? null - } - }, - { size, objectName, wsIds } - ) - // Less 5Mb - } else { - return ctx.with( - 'multipart-upload', - {}, - async () => { - const uploadTask = new Upload({ - client: this.client, - params: { - Bucket: this.getBucketId(wsIds), - Key: this.getDocumentKey(wsIds, objectName), - ContentType: contentType, - Body: stream - }, - - // (optional) concurrency configuration - // queueSize: 1, - - // (optional) size of each part, in bytes, at least 5MB - partSize: 1024 * 1024 * 5, - leavePartsOnError: false - }) - - const output = await uploadTask.done() - return { - etag: output.ETag ?? '', - versionId: output.VersionId ?? null - } - }, - { size, objectName, wsIds } - ) - } - } - - @withContext('read') - async read (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise { - const data = await this.doGet(ctx, wsIds, name) - const chunks: Buffer[] = [] - - await new Promise((resolve, reject) => { - data.on('data', (chunk) => { - chunks.push(chunk) - }) - - data.on('end', () => { - data.destroy() - resolve(null) - }) - data.on('error', (err) => { - data.destroy() - reject(err) - }) - }) - return chunks - } - - @withContext('partial') - async partial ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - offset: number, - length?: number - ): Promise { - const range = length !== undefined ? `bytes=${offset}-${offset + length}` : `bytes=${offset}-` - return await this.doGet(ctx, wsIds, objectName, range) - } - - @withContext('getUrl') - async getUrl (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - const filesUrl = getMetadata(serverCore.metadata.FilesUrl) ?? '' - return filesUrl.replaceAll(':workspace', getDataId(wsIds)).replaceAll(':blobId', objectName) - } -} - -export function processConfigFromEnv (storageConfig: StorageConfiguration): string | undefined { - const endpoint = process.env.S3_ENDPOINT - if (endpoint === undefined) { - return 'S3_ENDPOINT' - } - const accessKey = process.env.S3_ACCESS_KEY - if (accessKey === undefined) { - return 'S3_ACCESS_KEY' - } - - const secretKey = process.env.S3_SECRET_KEY - if (secretKey === undefined) { - return 'S3_SECRET_KEY' - } - - const config: S3Config = { - kind: 's3', - name: 's3', - region: 'auto', - endpoint, - accessKey, - secretKey - } - storageConfig.storages.push(config) - storageConfig.default = 's3' -} diff --git a/server/s3/src/perfTest.ts b/server/s3/src/perfTest.ts deleted file mode 100644 index ad31fd9c8e7..00000000000 --- a/server/s3/src/perfTest.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { MeasureMetricsContext, type WorkspaceDataId, type WorkspaceUuid, generateId } from '@hcengineering/core' -import type { StorageConfiguration } from '@hcengineering/server-core' -import { S3Service, processConfigFromEnv, type S3Config } from '.' - -const MB = 1024 * 1024 - -const config: StorageConfiguration = { default: 'minio', storages: [] } -const minioConfigVar = processConfigFromEnv(config) -if (minioConfigVar !== undefined || config.storages[0] === undefined) { - console.error('No S3 config env is configured:' + minioConfigVar) - it.skip('No S3 config env is configured', async () => {}) - process.exit(1) -} -const toolCtx = new MeasureMetricsContext('test', {}) -const storageService = new S3Service({ ...(config.storages[0] as S3Config), rootBucket: 'haiodo-test-bucket' }) - -async function doTest (): Promise { - const existingTestBuckets = await storageService.listBuckets(toolCtx) - // Delete old buckets - for (const b of existingTestBuckets) { - await b.delete() - } - - const genWorkspaceId1 = generateId() as unknown as WorkspaceDataId - - const wsIds1 = { - uuid: genWorkspaceId1 as unknown as WorkspaceUuid, - dataId: genWorkspaceId1, - url: '' - } - await storageService.make(toolCtx, wsIds1) - /// /////// Uploads - let st1 = Date.now() - const sz = 10 - const stream = Buffer.alloc(sz * 1024 * 1024) - for (let i = 0; i < 10; i++) { - // We need 1Mb random file to check upload speed. - const st = Date.now() - await storageService.put(toolCtx, wsIds1, `testObject.${i}`, stream, 'application/octet-stream', stream.length) - console.log('upload time', Date.now() - st) - } - let now = Date.now() - console.log(`upload performance: ${Math.round((sz * 10 * 1000 * 100) / (now - st1)) / 100} mb per second`) - - /// // Downloads 1 - st1 = Date.now() - for (let i = 0; i < 10; i++) { - // We need 1Mb random file to check upload speed. - const st = Date.now() - await storageService.read(toolCtx, wsIds1, `testObject.${i}`) - console.log('download time', Date.now() - st) - } - - now = Date.now() - console.log(`download performance: ${Math.round((sz * 10 * 1000 * 100) / (now - st1)) / 100} mb per second`) - - /// Downloads 2 - st1 = Date.now() - for (let i = 0; i < 10; i++) { - // We need 1Mb random file to check upload speed. - const st = Date.now() - const readable = await storageService.get(toolCtx, wsIds1, `testObject.${i}`) - const chunks: Buffer[] = [] - readable.on('data', (chunk) => { - chunks.push(chunk) - }) - await new Promise((resolve) => { - readable.on('end', () => { - resolve() - readable.destroy() - }) - }) - console.log('download time 2', Date.now() - st) - } - - now = Date.now() - console.log(`download performance: ${Math.round((sz * 10 * 1000 * 100) / (now - st1)) / 100} mb per second`) - - /// Downloads 3 - st1 = Date.now() - for (let i = 0; i < 10; i++) { - // We need 1Mb random file to check upload speed. - const st = Date.now() - for (let i = 0; i < sz; i++) { - const readable = await storageService.partial(toolCtx, wsIds1, `testObject.${i}`, i * MB, MB) - const chunks: Buffer[] = [] - readable.on('data', (chunk) => { - chunks.push(chunk) - }) - await new Promise((resolve) => { - readable.on('end', () => { - resolve() - readable.destroy() - }) - }) - } - console.log('download time 2', Date.now() - st) - } - - now = Date.now() - console.log(`download performance: ${Math.round((sz * 10 * 1000 * 100) / (now - st1)) / 100} mb per second`) -} -void doTest().catch((err) => { - console.error(err) -}) -console.log('done') diff --git a/server/s3/tsconfig.json b/server/s3/tsconfig.json deleted file mode 100644 index c6a877cf6c3..00000000000 --- a/server/s3/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/server-pipeline/package.json b/server/server-pipeline/package.json index 5af859bbbd7..b627c54a67b 100644 --- a/server/server-pipeline/package.json +++ b/server/server-pipeline/package.json @@ -95,7 +95,7 @@ "@hcengineering/server-controlled-documents-resources": "^0.7.0", "@hcengineering/server-training": "^0.7.0", "@hcengineering/server-training-resources": "^0.7.0", - "@hcengineering/middleware": "^0.7.0", + "@hcengineering/middleware": "^0.7.1", "@hcengineering/login-assets": "^0.7.0", "@hcengineering/onboard-assets": "^0.7.0", "@hcengineering/view-assets": "^0.7.0", diff --git a/server/server-pipeline/src/pipeline.ts b/server/server-pipeline/src/pipeline.ts index d2e1a32347c..b3a5c9c0bc3 100644 --- a/server/server-pipeline/src/pipeline.ts +++ b/server/server-pipeline/src/pipeline.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/unbound-method */ +import card from '@hcengineering/card' import { DOMAIN_BENCHMARK, DOMAIN_BLOB, @@ -9,7 +10,10 @@ import { ModelDb, systemAccountUuid, type Branding, + type Class, + type Doc, type MeasureContext, + type Ref, type Tx, type WorkspaceIds } from '@hcengineering/core' @@ -24,6 +28,7 @@ import { DomainTxMiddleware, FindSecurityMiddleware, FullTextMiddleware, + GuestPermissionsMiddleware, IdentityMiddleware, LiveQueryMiddleware, LookupMiddleware, @@ -31,6 +36,7 @@ import { MarkDerivedEntryMiddleware, ModelMiddleware, ModifiedMiddleware, + NormalizeTxMiddleware, PluginConfigurationMiddleware, PrivateMiddleware, QueryJoinMiddleware, @@ -39,9 +45,7 @@ import { SpaceSecurityMiddleware, TriggersMiddleware, TxMiddleware, - UserStatusMiddleware, - GuestPermissionsMiddleware, - NormalizeTxMiddleware + UserStatusMiddleware } from '@hcengineering/middleware' import { createBenchmarkAdapter, @@ -88,6 +92,19 @@ export function getTxAdapterFactory ( return adapter.factory } +function addMessagesToFullText (fulltext: MiddlewareCreator): MiddlewareCreator { + return async (ctx: MeasureContext, context: PipelineContext, next?: Middleware) => { + const result: FullTextMiddleware = (await fulltext(ctx, context, next)) as FullTextMiddleware + result.addExtraFind = (baseClass, childClasses) => { + if (context.hierarchy.isDerived(baseClass, card.class.Card)) { + // Using Card as base class because messages are the same for any card subclass + childClasses.add(`${card.class.Card}%message` as Ref>) + } + } + return result + } +} + /** * @public */ @@ -141,9 +158,11 @@ export function createServerPipeline ( ...(opt.disableTriggers === true ? [] : [TriggersMiddleware.create]), ...(opt.fulltextUrl !== undefined ? [ - FullTextMiddleware.create( - opt.fulltextUrl, - generateToken(systemAccountUuid, workspace.uuid, { service: 'transactor' }) + addMessagesToFullText( + FullTextMiddleware.create( + opt.fulltextUrl, + generateToken(systemAccountUuid, workspace.uuid, { service: 'transactor' }) + ) ) ] : []), diff --git a/server/server-storage/.eslintrc.js b/server/server-storage/.eslintrc.js deleted file mode 100644 index 72235dc2833..00000000000 --- a/server/server-storage/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/server-storage/.npmignore b/server/server-storage/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/server-storage/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/server-storage/config/rig.json b/server/server-storage/config/rig.json deleted file mode 100644 index 0110930f55e..00000000000 --- a/server/server-storage/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/server/server-storage/jest.config.js b/server/server-storage/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/server-storage/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/server-storage/package.json b/server/server-storage/package.json deleted file mode 100644 index 220ff9d1f0e..00000000000 --- a/server/server-storage/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "@hcengineering/server-storage", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "cross-env": "~7.0.3", - "@hcengineering/platform-rig": "^0.7.10", - "@types/node": "^22.15.29", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "ts-node": "^10.8.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" - }, - "dependencies": { - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "@hcengineering/mongo": "^0.7.0", - "@hcengineering/minio": "^0.7.0", - "@hcengineering/s3": "^0.7.0", - "@hcengineering/datalake": "^0.7.0", - "@hcengineering/storage": "^0.7.3", - "@hcengineering/analytics": "^0.7.3", - "@hcengineering/server-token": "^0.7.0" - } -} diff --git a/server/server-storage/src/fallback.ts b/server/server-storage/src/fallback.ts deleted file mode 100644 index 354adff9c88..00000000000 --- a/server/server-storage/src/fallback.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { - withContext, - type WorkspaceIds, - type Blob, - type MeasureContext, - type StorageIterator -} from '@hcengineering/core' -import { type Readable } from 'stream' - -import { getMetadata } from '@hcengineering/platform' -import { - type BlobStorageIterator, - type BucketInfo, - type ListBlobResult, - type NamedStorageAdapter, - type StorageAdapter, - type StorageAdapterEx, - type UploadedObjectInfo -} from '@hcengineering/storage' - -import { Analytics } from '@hcengineering/analytics' -import serverCore, { getDataId, type StorageConfig, type StorageConfiguration } from '@hcengineering/server-core' - -class NoSuchKeyError extends Error { - code: string - constructor (msg: string) { - super(msg) - this.code = 'NoSuchKey' - } -} - -/** - * Perform operations on storage adapter and map required information into BinaryDocument into provided DbAdapter storage. - */ -export class FallbackStorageAdapter implements StorageAdapter, StorageAdapterEx { - // Adapters should be in reverse order, first one is target one, and next ones are for fallback - constructor (readonly adapters: NamedStorageAdapter[]) {} - - async initialize (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} - - doTrimHash (s: string | undefined): string { - if (s == null) { - return '' - } - if (s.startsWith('"') && s.endsWith('"')) { - return s.slice(1, s.length - 1) - } - return s - } - - find (ctx: MeasureContext, wsIds: WorkspaceIds): StorageIterator { - const storageIterator = this.makeStorageIterator(ctx, wsIds) - - return { - next: async () => { - const docInfos = await storageIterator.next() - - return docInfos.map((it) => ({ - hash: it.etag, - id: it._id, - size: it.size, - contentType: it.contentType - })) - }, - close: async (ctx) => { - await storageIterator.close() - } - } - } - - private makeStorageIterator (ctx: MeasureContext, wsIds: WorkspaceIds): BlobStorageIterator { - // We need to reverse, since we need to iterate on latest document last - const adapters = [...this.adapters].reverse() - let provider: NamedStorageAdapter | undefined - let iterator: BlobStorageIterator | undefined - return { - next: async () => { - while (true) { - if (iterator === undefined && adapters.length > 0) { - provider = adapters.shift() as NamedStorageAdapter - iterator = await provider.adapter.listStream(ctx, wsIds) - } - if (iterator === undefined) { - return [] - } - const docInfos = await iterator.next() - if (docInfos.length > 0) { - for (const d of docInfos) { - d.provider = provider?.name as string - } - // We need to check if our stored version is fine - return docInfos - } else { - // We need to take next adapter - await iterator.close() - iterator = undefined - continue - } - } - }, - close: async () => { - if (iterator !== undefined) { - await iterator.close() - } - } - } - } - - async close (): Promise { - for (const { adapter } of this.adapters) { - await adapter.close() - } - } - - async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - for (const { adapter } of this.adapters) { - if (!(await adapter.exists(ctx, wsIds))) { - return false - } - } - return true - } - - @withContext('aggregator-make', {}) - async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - for (const { name, adapter } of this.adapters) { - try { - if (!(await adapter.exists(ctx, wsIds))) { - await adapter.make(ctx, wsIds) - } - } catch (err: any) { - ctx.error('failed to init adapter', { adapter: name, wsIds, error: err }) - // Do not throw error in case default adapter is ok - Analytics.handleError(err) - } - } - } - - @withContext('aggregator-listBuckets', {}) - async listBuckets (ctx: MeasureContext): Promise { - const result: BucketInfo[] = [] - for (const { adapter } of this.adapters) { - result.push(...(await adapter.listBuckets(ctx))) - } - return result - } - - @withContext('fallback-delete', {}) - async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - for (const { adapter } of this.adapters) { - if (await adapter.exists(ctx, wsIds)) { - await adapter.delete(ctx, wsIds) - } - } - } - - @withContext('fallback-remove', {}) - async remove (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]): Promise { - // Group by provider and delegate into it. - for (const { adapter } of this.adapters) { - await adapter.remove(ctx, wsIds, objectNames) - } - } - - async listStream (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - const storageIterator = this.makeStorageIterator(ctx, wsIds) - return { - next: async (): Promise => { - return await storageIterator.next() - }, - close: async () => { - await storageIterator.close() - } - } - } - - @withContext('fallback-stat', {}) - async stat (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - for (const { name, adapter } of this.adapters) { - const stat = await adapter.stat(ctx, wsIds, objectName) - if (stat !== undefined) { - stat.provider = name - return stat - } - } - } - - @withContext('fallback-get', {}) - async get (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - for (const { adapter } of this.adapters) { - try { - return await adapter.get(ctx, wsIds, objectName) - } catch (err: any) { - // ignore - } - } - throw new NoSuchKeyError(`uuid=${wsIds.uuid} dataId=${wsIds.dataId} missing ${objectName}`) - } - - @withContext('fallback-partial', {}) - async partial ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - offset: number, - length?: number | undefined - ): Promise { - for (const { adapter } of this.adapters) { - try { - return await adapter.partial(ctx, wsIds, objectName, offset, length) - } catch (err: any) { - // ignore - } - } - throw new NoSuchKeyError(`uuid=${wsIds.uuid} dataId=${wsIds.dataId} missing ${objectName}`) - } - - @withContext('fallback-read', {}) - async read (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - for (const { adapter } of this.adapters) { - try { - return await adapter.read(ctx, wsIds, objectName) - } catch (err: any) { - // Ignore - } - } - throw new NoSuchKeyError(`uuid=${wsIds.uuid} dataId=${wsIds.dataId} missing ${objectName}`) - } - - @withContext('aggregator-put', {}) - put ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - stream: string | Readable | Buffer, - contentType: string, - size?: number | undefined - ): Promise { - const adapter = this.adapters[0].adapter - // Remove in other storages, if appicable - return adapter.put(ctx, wsIds, objectName, stream, contentType, size) - } - - @withContext('aggregator-getUrl', {}) - async getUrl (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise { - const stat = await this.stat(ctx, wsIds, name) - if (stat !== undefined) { - for (const adapter of this.adapters) { - if (adapter.name === stat.provider) { - return await adapter.adapter.getUrl(ctx, wsIds, name) - } - } - } - - const filesUrl = getMetadata(serverCore.metadata.FilesUrl) ?? '' - return filesUrl.replaceAll(':workspace', getDataId(wsIds)).replaceAll(':blobId', name) - } -} - -/** - * @public - */ -export function buildStorage ( - config: StorageConfiguration, - storageFactory: (config: StorageConfig) => StorageAdapter -): FallbackStorageAdapter { - const adapters: NamedStorageAdapter[] = [] - for (const c of config.storages) { - adapters.push({ name: c.name, adapter: storageFactory(c) }) - } - // Reverse adapter's so latest one will be target one. - return new FallbackStorageAdapter(adapters.reverse()) -} diff --git a/server/server-storage/src/index.ts b/server/server-storage/src/index.ts deleted file mode 100644 index 092ed856874..00000000000 --- a/server/server-storage/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021, 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './fallback' -export * from './starter' diff --git a/server/server-storage/src/readonly.ts b/server/server-storage/src/readonly.ts deleted file mode 100644 index 978aa7d89fd..00000000000 --- a/server/server-storage/src/readonly.ts +++ /dev/null @@ -1,98 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { WorkspaceIds, Blob, MeasureContext } from '@hcengineering/core' -import type { BlobStorageIterator, BucketInfo, StorageAdapter, UploadedObjectInfo } from '@hcengineering/storage' -import { type Readable } from 'stream' - -class ReadonlyError extends Error { - constructor () { - super('Readonly mode') - this.name = 'ReadonlyError' - } -} - -export class ReadonlyStorageAdapter implements StorageAdapter { - constructor (private readonly adapter: StorageAdapter) {} - - async initialize (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - await this.adapter.initialize(ctx, wsIds) - } - - async close (): Promise { - await this.adapter.close() - } - - async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - return await this.adapter.exists(ctx, wsIds) - } - - async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - throw new ReadonlyError() - } - - async listBuckets (ctx: MeasureContext): Promise { - return await this.adapter.listBuckets(ctx) - } - - async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - throw new ReadonlyError() - } - - async remove (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]): Promise { - throw new ReadonlyError() - } - - async listStream (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - return await this.adapter.listStream(ctx, wsIds) - } - - async stat (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - return await this.adapter.stat(ctx, wsIds, objectName) - } - - async get (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - return await this.adapter.get(ctx, wsIds, objectName) - } - - async partial ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - offset: number, - length?: number | undefined - ): Promise { - return await this.adapter.partial(ctx, wsIds, objectName, offset, length) - } - - async read (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - return await this.adapter.read(ctx, wsIds, objectName) - } - - put ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - stream: string | Readable | Buffer, - contentType: string, - size?: number | undefined - ): Promise { - throw new ReadonlyError() - } - - async getUrl (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise { - return await this.adapter.getUrl(ctx, wsIds, name) - } -} diff --git a/server/server-storage/src/starter.ts b/server/server-storage/src/starter.ts deleted file mode 100644 index ac33f7b306e..00000000000 --- a/server/server-storage/src/starter.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { CONFIG_KIND as DATALAKE_CONFIG_KIND, DatalakeService, type DatalakeConfig } from '@hcengineering/datalake' -import { CONFIG_KIND as MINIO_CONFIG_KIND, MinioConfig, MinioService, addMinioFallback } from '@hcengineering/minio' -import { CONFIG_KIND as S3_CONFIG_KIND, S3Service, type S3Config } from '@hcengineering/s3' -import { StorageAdapter, StorageConfiguration, type StorageConfig } from '@hcengineering/server-core' -import { FallbackStorageAdapter, buildStorage } from './fallback' -import { ReadonlyStorageAdapter } from './readonly' - -/* - - A ';' separated list of URI's to configure the storage adapters. A new lines will be ommited during parse. - - Each config is in `kind(,name)?|uri|contentTypes` format. - - * kind - an storage kind minior/s3 for now. - * name - a symbolic name for provider, name could be ommited in case kind will be used as name. - * uri - an storage URI with encoded parameters. - - Last one is used as default one, or one with conrent type matched will be used. - - Example: - STORAGE_CONFIG=kind|minio|minio:9000?accessKey=minio&secretKey=minio&useSSL=false;\ - s3|https://s3.amazonaws.com?accessKey=${ACCESS_KEY}&secretKey=${SECRET_KEY}®ion=us-east-1 - -*/ - -export function storageConfigFromEnv (configEnv?: string): StorageConfiguration { - const storageConfig: StorageConfiguration = { default: '', storages: [] } - - const storageEnv = configEnv ?? process.env.STORAGE_CONFIG - if (storageEnv !== undefined) { - parseStorageEnv(storageEnv, storageConfig) - } - - if (storageConfig.storages.length === 0 || storageConfig.default === '') { - // 'STORAGE_CONFIG is required for complex configuration, fallback to minio config' - addMinioFallback(storageConfig) - } - return storageConfig -} - -export function parseStorageEnv (storageEnv: string, storageConfig: StorageConfiguration): void { - const storages = storageEnv.split(';') - for (const st of storages) { - if (st.trim().length === 0 || !st.includes('|')) { - throw new Error('Invalid storage config:' + st) - } - let [kindName, url] = st.split('|') - let [kind, name] = kindName.split(',') - if (name == null) { - name = kind - } - let hasProtocol = true - if (!url.includes('://')) { - // No protocol, add empty one - url = 'empty://' + url - hasProtocol = false - } - const uri = new URL(url) - const config: StorageConfig = { - kind, - name, - endpoint: (hasProtocol ? uri.protocol + '//' : '') + uri.hostname, // Port should go away - port: uri.port !== '' ? parseInt(uri.port) : undefined - } - - // Add all extra parameters - uri.searchParams.forEach((v, k) => { - ;(config as any)[k] = v - }) - - if (storageConfig.storages.find((it) => it.name === config.name) !== undefined) { - throw new Error(`Duplicated storage name ${config.name}, skipping config:${st}`) - } - storageConfig.storages.push(config) - storageConfig.default = config.name - } -} - -export function createStorageFromConfig (config: StorageConfig): StorageAdapter { - let adapter: StorageAdapter - const kind = config.kind - if (kind === MINIO_CONFIG_KIND) { - const c = config as MinioConfig - if (c.endpoint == null || c.accessKey == null || c.secretKey == null) { - throw new Error('One of endpoint/accessKey/secretKey values are not specified') - } - adapter = new MinioService(c) - } else if (kind === S3_CONFIG_KIND) { - const c = config as S3Config - if (c.endpoint == null || c.accessKey == null || c.secretKey == null) { - throw new Error('One of endpoint/accessKey/secretKey values are not specified') - } - adapter = new S3Service(c) - } else if (kind === DATALAKE_CONFIG_KIND) { - const c = config as DatalakeConfig - if (c.endpoint == null) { - throw new Error('Endpoint value is not specified') - } - adapter = new DatalakeService(c) - } else { - throw new Error('Unsupported storage kind:' + kind) - } - - if (config.readonly === 'true') { - adapter = new ReadonlyStorageAdapter(adapter) - } - - return adapter -} - -export function buildStorageFromConfig (config: StorageConfiguration): FallbackStorageAdapter { - return buildStorage(config, createStorageFromConfig) -} diff --git a/server/server-storage/src/tests/aggregator.spec.ts b/server/server-storage/src/tests/aggregator.spec.ts deleted file mode 100644 index 8d75460c5f5..00000000000 --- a/server/server-storage/src/tests/aggregator.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - MeasureMetricsContext, - type WorkspaceDataId, - type MeasureContext, - type WorkspaceUuid, - type WorkspaceIds -} from '@hcengineering/core' -import type { NamedStorageAdapter } from '@hcengineering/storage' -import { FallbackStorageAdapter } from '../fallback' -import { MemStorageAdapter } from './memAdapters' - -describe('aggregator tests', () => { - function prepare1 (): { - mem1: MemStorageAdapter - mem2: MemStorageAdapter - aggr: FallbackStorageAdapter - testCtx: MeasureContext - wsIds1: WorkspaceIds - } { - const mem1 = new MemStorageAdapter() - - const mem2 = new MemStorageAdapter() - const adapters: NamedStorageAdapter[] = [] - adapters.push({ name: 'mem2', adapter: mem2 }) - adapters.push({ name: 'mem1', adapter: mem1 }) - const aggr = new FallbackStorageAdapter(adapters) - - const testCtx = new MeasureMetricsContext('test', {}) - const wsIds1 = { - uuid: 'ws1-uuid' as WorkspaceUuid, - dataId: 'ws1-dataId' as WorkspaceDataId, - url: 'ws1-url' - } - return { mem1, mem2, aggr, wsIds1, testCtx } - } - - it('not reuse existing storage', async () => { - const { mem1, aggr, wsIds1, testCtx } = prepare1() - - // Test default provider - await mem1.put(testCtx, wsIds1, 'test', 'data', 'text/plain') - - const stat = await aggr.stat(testCtx, wsIds1, 'test') - expect(stat?.provider).toEqual('mem1') - - await aggr.put(testCtx, wsIds1, 'test', 'data2', 'text/plain') - const stat2 = await aggr.stat(testCtx, wsIds1, 'test') - expect(stat2?.provider).toEqual('mem2') - - const dta = Buffer.concat((await aggr.read(testCtx, wsIds1, 'test')) as any).toString() - expect(dta).toEqual('data2') - }) -}) diff --git a/server/server-storage/src/tests/memAdapters.ts b/server/server-storage/src/tests/memAdapters.ts deleted file mode 100644 index 68671cea88e..00000000000 --- a/server/server-storage/src/tests/memAdapters.ts +++ /dev/null @@ -1,148 +0,0 @@ -import core, { - WorkspaceIds, - WorkspaceUuid, - type Blob, - type MeasureContext, - type WorkspaceDataId -} from '@hcengineering/core' -import { getDataId } from '@hcengineering/server-core' -import type { BlobStorageIterator, BucketInfo, StorageAdapter, UploadedObjectInfo } from '@hcengineering/storage' -import { Readable } from 'stream' - -export class MemStorageAdapter implements StorageAdapter { - files = new Map() - - async initialize (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} - - async close (): Promise {} - - async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - return true - } - - async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} - - async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} - - async listBuckets (ctx: MeasureContext): Promise { - const workspaces = new Set(Array.from(this.files.values()).map((it) => it.workspace)) - return Array.from(workspaces).map((it) => ({ - name: it, - delete: async () => { - await this.delete(ctx, { - uuid: it as unknown as WorkspaceUuid, - dataId: it, - url: '' - }) - }, - list: () => - this.listStream(ctx, { - uuid: it as unknown as WorkspaceUuid, - dataId: it, - url: '' - }) - })) - } - - async remove (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]): Promise { - for (const k of objectNames) { - this.files.delete(getDataId(wsIds) + '/' + k) - } - } - - async listStream (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - const files = Array.from(this.files.values()).filter((it) => it.workspace === getDataId(wsIds)) - return { - next: async () => { - return files.splice(0, 100) - }, - close: async () => {} - } - } - - async stat (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - return this.files.get(getDataId(wsIds) + '/' + objectName) - } - - async get (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - const readable = new Readable() - readable._read = () => {} - const content = this.files.get(getDataId(wsIds) + '/' + objectName)?.content - readable.push(content) - readable.push(null) - return readable - } - - async put ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - stream: string | Readable | Buffer, - contentType: string, - size?: number | undefined - ): Promise { - const buffer: Buffer[] = [] - if (stream instanceof Buffer) { - buffer.push(stream) - } else if (typeof stream === 'string') { - buffer.push(Buffer.from(stream)) - } else if (stream instanceof Readable) { - await new Promise((resolve, reject) => { - stream.on('end', () => { - resolve() - }) - stream.on('error', (error) => { - reject(error) - }) - stream.on('data', (data) => { - buffer.push(data) - resolve() - }) - }) - } - const data = Buffer.concat(buffer as any) - const dataId = getDataId(wsIds) - const dta = { - _class: core.class.Blob, - _id: objectName as any, - contentType, - size: data.length, - content: data, - etag: objectName, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - provider: '_test', - space: '' as any, - version: null, - workspace: dataId - } - this.files.set(dataId + '/' + objectName, dta) - return { - etag: objectName, - versionId: null - } - } - - async read (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - const content = this.files.get(getDataId(wsIds) + '/' + objectName)?.content - if (content === undefined) { - throw new Error('NoSuchKey') - } - return [content] - } - - partial ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - offset: number, - length?: number | undefined - ): Promise { - // Partial are not supported by - throw new Error('NoSuchKey') - } - - async getUrl (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - return '/files/' + objectName - } -} diff --git a/server/server-storage/src/tests/testConfig.spec.ts b/server/server-storage/src/tests/testConfig.spec.ts deleted file mode 100644 index e3474fdbea2..00000000000 --- a/server/server-storage/src/tests/testConfig.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { MinioConfig } from '@hcengineering/minio' -import type { S3Config } from '@hcengineering/s3' -import type { StorageConfiguration } from '@hcengineering/server-core' -import { parseStorageEnv } from '../starter' - -describe('config-parse', () => { - it('single-minio', async () => { - const cfg: StorageConfiguration = { default: '', storages: [] } - parseStorageEnv('minio|localhost:9000?accessKey=minio&secretKey=minio2', cfg) - expect(cfg.default).toEqual('minio') - const minio = cfg.storages[0] as MinioConfig - expect(minio.endpoint).toEqual('localhost') - expect(minio.port).toEqual(9000) - expect(minio.accessKey).toEqual('minio') - expect(minio.secretKey).toEqual('minio2') - }) - it('single-minio-named', async () => { - const cfg: StorageConfiguration = { default: '', storages: [] } - parseStorageEnv('minio,myminio|localhost:9000?accessKey=minio&secretKey=minio2', cfg) - expect(cfg.default).toEqual('myminio') - const minio = cfg.storages[0] as MinioConfig - expect(minio.endpoint).toEqual('localhost') - expect(minio.port).toEqual(9000) - expect(minio.accessKey).toEqual('minio') - expect(minio.secretKey).toEqual('minio2') - }) - it('single-s3-line', async () => { - const cfg: StorageConfiguration = { default: '', storages: [] } - parseStorageEnv('s3|https://s3.somehost.com?accessKey=minio&secretKey=minio2', cfg) - expect(cfg.default).toEqual('s3') - const minio = cfg.storages[0] as S3Config - expect(minio.endpoint).toEqual('https://s3.somehost.com') - expect(minio.port).toEqual(undefined) - expect(minio.accessKey).toEqual('minio') - expect(minio.secretKey).toEqual('minio2') - }) - it('multiple', async () => { - const cfg: StorageConfiguration = { default: '', storages: [] } - parseStorageEnv( - 'minio|localhost:9000?accessKey=minio&secretKey=minio2;s3|http://localhost?accessKey=minio&secretKey=minio2', - cfg - ) - expect(cfg.default).toEqual('s3') - expect(cfg.storages.length).toEqual(2) - }) - it('test-decode unexpected symbols', async () => { - const cfg: StorageConfiguration = { default: '', storages: [] } - parseStorageEnv( - 'minio|localhost:9000?accessKey=%F0%9F%91%85%F0%9F%91%BB%20-%20%D0%AD%D0%A2%D0%9E%20%20%20%20%D1%82%D0%B0%D0%BA%D0%BE%D0%B9%20%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C%0A%D0%90%20%D1%82%D0%BE&secretKey=minio2&downloadUrl=https%3A%2F%2Ffront.hc.engineering', - cfg - ) - expect(cfg.default).toEqual('minio') - const minio = cfg.storages[0] as MinioConfig - expect(minio.endpoint).toEqual('localhost') - expect(minio.port).toEqual(9000) - expect(minio.accessKey).toEqual('👅👻 - ЭТО такой пароль\nА то') - expect(minio.secretKey).toEqual('minio2') - expect((minio as any).downloadUrl).toEqual('https://front.hc.engineering') - }) -}) diff --git a/server/server-storage/tsconfig.json b/server/server-storage/tsconfig.json deleted file mode 100644 index b5ae22f6e46..00000000000 --- a/server/server-storage/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/server/server/.eslintrc.js b/server/server/.eslintrc.js deleted file mode 100644 index ce90fb9646f..00000000000 --- a/server/server/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/server/server/.npmignore b/server/server/.npmignore deleted file mode 100644 index e3ec093c383..00000000000 --- a/server/server/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/server/server/CHANGELOG.json b/server/server/CHANGELOG.json deleted file mode 100644 index 83b7a063490..00000000000 --- a/server/server/CHANGELOG.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@hcengineering/server", - "entries": [ - { - "version": "0.7.0", - "tag": "@hcengineering/server_v0.6.2", - "date": "Fri, 20 Aug 2021 16:21:03 GMT", - "comments": { - "patch": [ - { - "comment": "Transaction ordering" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `~0.6.10` to `~0.6.11`" - }, - { - "comment": "Updating dependency \"@hcengineering/mongo\" from `~0.6.0` to `~0.6.1`" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/server_v0.6.0", - "date": "Sun, 08 Aug 2021 10:14:57 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.3` to `~0.6.4`" - } - ] - } - } - ] -} diff --git a/server/server/CHANGELOG.md b/server/server/CHANGELOG.md deleted file mode 100644 index 692be47cbd1..00000000000 --- a/server/server/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -# Change Log - @hcengineering/server - -This log was last generated on Fri, 20 Aug 2021 16:21:03 GMT and should not be manually modified. - -## 0.6.2 -Fri, 20 Aug 2021 16:21:03 GMT - -### Patches - -- Transaction ordering - -## 0.6.0 -Sun, 08 Aug 2021 10:14:57 GMT - -_Initial release_ - diff --git a/server/server/config/rig.json b/server/server/config/rig.json deleted file mode 100644 index 78cc5a17334..00000000000 --- a/server/server/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/server/server/jest.config.js b/server/server/jest.config.js deleted file mode 100644 index 2cfd408b679..00000000000 --- a/server/server/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/server/server/package.json b/server/server/package.json deleted file mode 100644 index e10bdc633b8..00000000000 --- a/server/server/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "@hcengineering/server", - "version": "0.7.0", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --forceExit", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "cross-env": "~7.0.3", - "@hcengineering/platform-rig": "^0.7.10", - "@types/node": "^22.15.29", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.1.0", - "typescript": "^5.8.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" - }, - "dependencies": { - "@hcengineering/account-client": "^0.7.3", - "@hcengineering/analytics": "^0.7.3", - "@hcengineering/core": "^0.7.3", - "@hcengineering/platform": "^0.7.3", - "@hcengineering/rpc": "^0.7.3", - "@hcengineering/server-core": "^0.7.0", - "@hcengineering/server-token": "^0.7.0", - "utf-8-validate": "^6.0.4" - } -} diff --git a/server/server/src/blobs.ts b/server/server/src/blobs.ts deleted file mode 100644 index cc02245d181..00000000000 --- a/server/server/src/blobs.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { Analytics } from '@hcengineering/analytics' -import type { MeasureContext, WorkspaceIds } from '@hcengineering/core' -import type { StorageAdapter } from '@hcengineering/server-core' -import type { Readable } from 'stream' - -const cacheControlNoCache = 'public, no-store, no-cache, must-revalidate, max-age=0' - -export interface BlobResponse { - aborted: boolean - writeHead: (code: number, header: Record) => void - status: (code: number) => void - end: () => void - pipeFrom: (readable: Readable, size: number) => void - cork: (cb: () => void) => void -} - -export async function getFile ( - ctx: MeasureContext, - client: StorageAdapter, - wsIds: WorkspaceIds, - file: string, - res: BlobResponse -): Promise { - const stat = await ctx.with('stat', {}, () => client.stat(ctx, wsIds, file)) - if (stat === undefined) { - ctx.error('No such key', { file }) - res.cork(() => { - res.status(404) - res.end() - }) - return - } - - await ctx.with( - 'write', - { contentType: stat.contentType }, - async (ctx) => { - try { - const dataStream = await ctx.with('readable', {}, () => client.get(ctx, wsIds, file)) - await new Promise((resolve, reject) => { - res.cork(() => { - res.writeHead(200, { - 'Content-Type': stat.contentType, - Etag: stat.etag, - connection: 'keep-alive', - 'keep-alive': 'timeout=5, max=1000', - 'Last-Modified': new Date(stat.modifiedOn).toISOString(), - 'Cache-Control': cacheControlNoCache - }) - - res.pipeFrom(dataStream, stat.size) - dataStream.on('end', function () { - res.cork(() => { - res.end() - }) - dataStream.destroy() - resolve() - }) - dataStream.on('error', function (err) { - Analytics.handleError(err) - ctx.error('error', { err }) - reject(err) - }) - }) - }) - } catch (err: any) { - ctx.error('get-file-error', { workspace: wsIds, err }) - Analytics.handleError(err) - res.cork(() => { - res.status(500) - res.end() - }) - } - }, - {} - ) -} - -function getRange (range: string, size: number): [number, number] { - const [startStr, endStr] = range.replace(/bytes=/, '').split('-') - - let start = parseInt(startStr, 10) - let end = endStr !== undefined ? parseInt(endStr, 10) : size - 1 - - if (!isNaN(start) && isNaN(end)) { - end = size - 1 - } - - if (isNaN(start) && !isNaN(end)) { - start = size - end - end = size - 1 - } - - return [start, end] -} -export async function getFileRange ( - ctx: MeasureContext, - range: string, - client: StorageAdapter, - wsIds: WorkspaceIds, - uuid: string, - res: BlobResponse -): Promise { - const stat = await ctx.with('stats', {}, () => client.stat(ctx, wsIds, uuid)) - if (stat === undefined) { - ctx.error('No such key', { file: uuid }) - res.cork(() => { - res.status(404) - res.end() - }) - return - } - const size: number = stat.size - - let [start, end] = getRange(range, size) - - if (end >= size) { - end = size // Allow to iterative return of entire document - } - if (start >= size) { - res.cork(() => { - res.writeHead(416, { - 'Content-Range': `bytes */${size}`, - connection: 'keep-alive', - 'keep-alive': 'timeout=5, max=1000' - }) - res.end() - }) - return - } - - await ctx.with( - 'write', - { contentType: stat.contentType }, - async (ctx) => { - try { - const dataStream = await ctx.with( - 'partial', - {}, - () => client.partial(ctx, wsIds, uuid, start, end - start + 1), - {} - ) - await new Promise((resolve, reject) => { - res.cork(() => { - res.writeHead(206, { - 'Content-Range': `bytes ${start}-${end}/${size}`, - 'Accept-Ranges': 'bytes', - connection: 'keep-alive', - 'keep-alive': 'timeout=5, max=1000', - // 'Content-Length': end - start + 1, - 'Content-Type': stat.contentType, - Etag: stat.etag, - 'Last-Modified': new Date(stat.modifiedOn).toISOString() - }) - - res.pipeFrom(dataStream, end - start) - dataStream.on('end', function () { - res.cork(() => { - res.end() - }) - dataStream.destroy() - resolve() - }) - dataStream.on('error', function (err) { - Analytics.handleError(err) - ctx.error('error', { err }) - res.cork(() => { - res.end() - }) - reject(err) - }) - }) - }) - } catch (err: any) { - if ( - err?.code === 'NoSuchKey' || - err?.code === 'NotFound' || - err?.message === 'No such key' || - err?.Code === 'NoSuchKey' - ) { - ctx.info('No such key', { workspace: wsIds, uuid }) - res.cork(() => { - res.status(404) - res.end() - }) - return - } else { - Analytics.handleError(err) - ctx.error(err) - } - res.cork(() => { - res.status(500) - res.end() - }) - } - }, - { uuid, start, end: end - start + 1 } - ) -} diff --git a/server/server/src/client.ts b/server/server/src/client.ts deleted file mode 100644 index 1a109e2b21d..00000000000 --- a/server/server/src/client.ts +++ /dev/null @@ -1,456 +0,0 @@ -// -// Copyright © 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { LoginInfoWithWorkspaces } from '@hcengineering/account-client' -import { - generateId, - type PermissionsGrant, - TxProcessor, - type Account, - type AccountUuid, - type Class, - type Doc, - type DocumentQuery, - type Domain, - type DomainParams, - type DomainResult, - type FindOptions, - type FindResult, - type LoadModelResponse, - type MeasureContext, - type OperationDomain, - type PersonId, - type Ref, - type SearchOptions, - type SearchQuery, - type SearchResult, - type SessionData, - type SocialId, - type Timestamp, - type Tx, - type TxCUD, - type TxResult, - type WorkspaceDataId, - type WorkspaceIds, - type Space -} from '@hcengineering/core' -import { PlatformError, unknownError } from '@hcengineering/platform' -import { - BackupClientOps, - createBroadcastEvent, - SessionDataImpl, - type ClientSessionCtx, - type ConnectionSocket, - type Pipeline, - type Session, - type SessionRequest, - type StatisticsElement -} from '@hcengineering/server-core' -import { type Token } from '@hcengineering/server-token' - -const useReserveContext = (process.env.USE_RESERVE_CTX ?? 'true') === 'true' - -/** - * @public - */ -export class ClientSession implements Session { - createTime = Date.now() - requests = new Map() - binaryMode: boolean = false - useCompression: boolean = false - sessionId = '' - lastRequest = Date.now() - - lastPing: number = Date.now() - - total: StatisticsElement = { find: 0, tx: 0 } - current: StatisticsElement = { find: 0, tx: 0 } - mins5: StatisticsElement = { find: 0, tx: 0 } - measures: { id: string, message: string, time: 0 }[] = [] - - ops: BackupClientOps | undefined - opsPipeline: Pipeline | undefined - isAdmin: boolean - - constructor ( - readonly token: Token, - readonly workspace: WorkspaceIds, - readonly account: Account, - readonly info: LoginInfoWithWorkspaces, - readonly allowUpload: boolean - ) { - this.isAdmin = this.token.extra?.admin === 'true' - } - - getUser (): AccountUuid { - return this.token.account - } - - getUserSocialIds (): PersonId[] { - return this.account.socialIds - } - - getSocialIds (): SocialId[] { - return this.info.socialIds - } - - getRawAccount (): Account { - return this.account - } - - isUpgradeClient (): boolean { - return this.token.extra?.model === 'upgrade' - } - - getMode (): string { - return this.token.extra?.mode ?? 'normal' - } - - updateLast (): void { - this.lastRequest = Date.now() - } - - async ping (ctx: ClientSessionCtx): Promise { - this.lastRequest = Date.now() - ctx.sendPong() - } - - async loadModel (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string): Promise { - try { - this.includeSessionContext(ctx) - const result = await ctx.pipeline.loadModel(ctx.ctx, lastModelTx, hash) - await ctx.sendResponse(ctx.requestId, result) - } catch (err) { - await ctx.sendError(ctx.requestId, 'Failed to loadModel', unknownError(err)) - ctx.ctx.error('failed to loadModel', { err }) - } - } - - async loadModelRaw (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string): Promise { - this.includeSessionContext(ctx) - return await ctx.ctx.with('load-model', {}, (_ctx) => ctx.pipeline.loadModel(_ctx, lastModelTx, hash)) - } - - private getPermissionsGrant (): PermissionsGrant | undefined { - if (this.token.grant == null) { - return - } - - return { - spaces: this.token.grant?.spaces as Ref[] | undefined, - grantedBy: this.token.grant?.grantedBy - } - } - - includeSessionContext (ctx: ClientSessionCtx): void { - const dataId = this.workspace.dataId ?? (this.workspace.uuid as unknown as WorkspaceDataId) - const contextData = new SessionDataImpl( - this.account, - this.sessionId, - this.isAdmin, - undefined, - { - ...this.workspace, - dataId - }, - false, - undefined, - undefined, - ctx.pipeline.context.modelDb, - ctx.socialStringsToUsers, - this.token.extra?.service ?? '🤦‍♂️user', - this.getPermissionsGrant() - ) - ctx.ctx.contextData = contextData - } - - findAllRaw( - ctx: ClientSessionCtx, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - this.lastRequest = Date.now() - this.total.find++ - this.current.find++ - this.includeSessionContext(ctx) - return ctx.pipeline.findAll(ctx.ctx, _class, query, options) - } - - async findAll( - ctx: ClientSessionCtx, - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise { - try { - await ctx.sendResponse(ctx.requestId, await this.findAllRaw(ctx, _class, query, options)) - } catch (err) { - await ctx.sendError(ctx.requestId, 'Failed to findAll', unknownError(err)) - ctx.ctx.error('failed to findAll', { err }) - } - } - - async searchFulltext (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions): Promise { - try { - this.lastRequest = Date.now() - this.includeSessionContext(ctx) - await ctx.sendResponse(ctx.requestId, await ctx.pipeline.searchFulltext(ctx.ctx, query, options)) - } catch (err) { - await ctx.sendError(ctx.requestId, 'Failed to searchFulltext', unknownError(err)) - ctx.ctx.error('failed to searchFulltext', { err }) - } - } - - async searchFulltextRaw (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions): Promise { - this.lastRequest = Date.now() - this.includeSessionContext(ctx) - return await ctx.pipeline.searchFulltext(ctx.ctx, query, options) - } - - async txRaw ( - ctx: ClientSessionCtx, - tx: Tx - ): Promise<{ - result: TxResult - broadcastPromise: Promise - asyncsPromise: Promise | undefined - }> { - this.lastRequest = Date.now() - this.total.tx++ - this.current.tx++ - this.includeSessionContext(ctx) - - let cid = 'client_' + generateId() - ctx.ctx.id = cid - let onEnd = useReserveContext ? ctx.pipeline.context.adapterManager?.reserveContext?.(cid) : undefined - let result: TxResult - try { - result = await ctx.pipeline.tx(ctx.ctx, [tx]) - } finally { - onEnd?.() - } - // Send result immideately - await ctx.sendResponse(ctx.requestId, result) - - // We need to broadcast all collected transactions - const broadcastPromise = ctx.pipeline.handleBroadcast(ctx.ctx) - - // ok we could perform async requests if any - const asyncs = (ctx.ctx.contextData as SessionData).asyncRequests ?? [] - let asyncsPromise: Promise | undefined - if (asyncs.length > 0) { - cid = 'client_async_' + generateId() - ctx.ctx.id = cid - onEnd = useReserveContext ? ctx.pipeline.context.adapterManager?.reserveContext?.(cid) : undefined - const handleAyncs = async (): Promise => { - try { - for (const r of asyncs) { - await r(ctx.ctx, cid) - } - } finally { - onEnd?.() - } - } - asyncsPromise = handleAyncs() - } - - return { result, broadcastPromise, asyncsPromise } - } - - async tx (ctx: ClientSessionCtx, tx: Tx): Promise { - try { - const { broadcastPromise, asyncsPromise } = await this.txRaw(ctx, tx) - await broadcastPromise - if (asyncsPromise !== undefined) { - await asyncsPromise - } - } catch (err) { - await ctx.sendError(ctx.requestId, 'Failed to tx', unknownError(err)) - ctx.ctx.error('failed to tx', { err }) - } - } - - broadcast (ctx: MeasureContext, socket: ConnectionSocket, tx: Tx[]): void { - if (this.tx.length > 10000) { - const classes = new Set>>() - for (const dtx of tx) { - if (TxProcessor.isExtendsCUD(dtx._class)) { - classes.add((dtx as TxCUD).objectClass) - const attachedToClass = (dtx as TxCUD).attachedToClass - if (attachedToClass !== undefined) { - classes.add(attachedToClass) - } - } - } - const bevent = createBroadcastEvent(Array.from(classes)) - void socket.send( - ctx, - { - result: [bevent] - }, - this.binaryMode, - this.useCompression - ) - } else { - void socket.send(ctx, { result: tx }, this.binaryMode, this.useCompression) - } - } - - getOps (pipeline: Pipeline): BackupClientOps { - if (this.ops === undefined || this.opsPipeline !== pipeline) { - if (pipeline.context.lowLevelStorage === undefined) { - throw new PlatformError(unknownError('Low level storage is not available')) - } - this.ops = new BackupClientOps(pipeline.context.lowLevelStorage) - this.opsPipeline = pipeline - } - return this.ops - } - - async loadChunk (ctx: ClientSessionCtx, domain: Domain, idx?: number): Promise { - this.lastRequest = Date.now() - try { - const result = await this.getOps(ctx.pipeline).loadChunk(ctx.ctx, domain, idx) - await ctx.sendResponse(ctx.requestId, result) - } catch (err: any) { - await ctx.sendError(ctx.requestId, 'Failed to upload', unknownError(err)) - ctx.ctx.error('failed to loadChunk', { domain, err }) - } - } - - async getDomainHash (ctx: ClientSessionCtx, domain: Domain): Promise { - this.lastRequest = Date.now() - try { - const result = await this.getOps(ctx.pipeline).getDomainHash(ctx.ctx, domain) - await ctx.sendResponse(ctx.requestId, result) - } catch (err: any) { - await ctx.sendError(ctx.requestId, 'Failed to upload', unknownError(err)) - ctx.ctx.error('failed to getDomainHash', { domain, err }) - } - } - - async closeChunk (ctx: ClientSessionCtx, idx: number): Promise { - try { - this.lastRequest = Date.now() - await this.getOps(ctx.pipeline).closeChunk(ctx.ctx, idx) - await ctx.sendResponse(ctx.requestId, {}) - } catch (err: any) { - await ctx.sendError(ctx.requestId, 'Failed to closeChunk', unknownError(err)) - ctx.ctx.error('failed to closeChunk', { err }) - } - } - - async loadDocs (ctx: ClientSessionCtx, domain: Domain, docs: Ref[]): Promise { - this.lastRequest = Date.now() - try { - const result = await this.getOps(ctx.pipeline).loadDocs(ctx.ctx, domain, docs) - await ctx.sendResponse(ctx.requestId, result) - } catch (err: any) { - await ctx.sendError(ctx.requestId, 'Failed to loadDocs', unknownError(err)) - ctx.ctx.error('failed to loadDocs', { domain, err }) - } - } - - async upload (ctx: ClientSessionCtx, domain: Domain, docs: Doc[]): Promise { - if (!this.allowUpload) { - await ctx.sendResponse(ctx.requestId, { error: 'Upload not allowed' }) - } - this.lastRequest = Date.now() - try { - await this.getOps(ctx.pipeline).upload(ctx.ctx, domain, docs) - } catch (err: any) { - await ctx.sendError(ctx.requestId, 'Failed to upload', unknownError(err)) - ctx.ctx.error('failed to loadDocs', { domain, err }) - return - } - await ctx.sendResponse(ctx.requestId, {}) - } - - async clean (ctx: ClientSessionCtx, domain: Domain, docs: Ref[]): Promise { - if (!this.allowUpload) { - await ctx.sendResponse(ctx.requestId, { error: 'Clean not allowed' }) - } - this.lastRequest = Date.now() - try { - await this.getOps(ctx.pipeline).clean(ctx.ctx, domain, docs) - } catch (err: any) { - await ctx.sendError(ctx.requestId, 'Failed to clean', unknownError(err)) - ctx.ctx.error('failed to clean', { domain, err }) - return - } - await ctx.sendResponse(ctx.requestId, {}) - } - - async domainRequest (ctx: ClientSessionCtx, domain: OperationDomain, params: DomainParams): Promise { - try { - const { asyncsPromise, broadcastPromise } = await this.domainRequestRaw(ctx, domain, params) - - await broadcastPromise - - if (asyncsPromise !== undefined) { - await asyncsPromise - } - } catch (err) { - await ctx.sendError(ctx.requestId, 'Failed to domainRequest', unknownError(err)) - ctx.ctx.error('failed to domainRequest', { err }) - } - } - - async domainRequestRaw ( - ctx: ClientSessionCtx, - domain: OperationDomain, - params: DomainParams - ): Promise<{ - result: DomainResult - broadcastPromise: Promise - asyncsPromise: Promise | undefined - }> { - this.lastRequest = Date.now() - this.total.find++ - this.current.find++ - this.includeSessionContext(ctx) - - const result: DomainResult = await ctx.pipeline.domainRequest(ctx.ctx, domain, params) - await ctx.sendResponse(ctx.requestId, result) - // We need to broadcast all collected transactions - const broadcastPromise = ctx.pipeline.handleBroadcast(ctx.ctx) - - // ok we could perform async requests if any - const asyncs = (ctx.ctx.contextData as SessionData).asyncRequests ?? [] - let asyncsPromise: Promise | undefined - if (asyncs.length > 0) { - const handleAyncs = async (): Promise => { - // Make sure the broadcast is complete before we start the asyncs - await broadcastPromise - ctx.ctx.contextData.broadcast.queue = [] - ctx.ctx.contextData.broadcast.txes = [] - ctx.ctx.contextData.broadcast.sessions = {} - try { - for (const r of asyncs) { - await r(ctx.ctx) - } - } catch (err: any) { - ctx.ctx.error('failed to handleAsyncs', { err }) - } - } - asyncsPromise = handleAyncs().then(async () => { - await ctx.pipeline?.handleBroadcast(ctx.ctx) - }) - } - - return { result, asyncsPromise, broadcastPromise } - } -} diff --git a/server/server/src/index.ts b/server/server/src/index.ts deleted file mode 100644 index 2fe0c8e1473..00000000000 --- a/server/server/src/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021, 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './blobs' -export * from './client' -export * from './sessionManager' -export * from './starter' -export * from './stats' -export * from './utils' diff --git a/server/server/src/sessionManager.ts b/server/server/src/sessionManager.ts deleted file mode 100644 index a35acae40f0..00000000000 --- a/server/server/src/sessionManager.ts +++ /dev/null @@ -1,1668 +0,0 @@ -// -// 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 { - getClient as getAccountClient, - type LoginInfoWithWorkspaces, - type LoginInfoWorkspace -} from '@hcengineering/account-client' -import { Analytics } from '@hcengineering/analytics' -import core, { - AccountRole, - type AccountUuid, - type Branding, - type BrandingMap, - cutObjectArray, - type Data, - generateId, - isArchivingMode, - isMigrationMode, - isRestoringMode, - isWorkspaceCreating, - type MeasureContext, - type PersonId, - pickPrimarySocialId, - platformNow, - platformNowDiff, - readOnlyGuestAccountUuid, - SocialIdType, - systemAccountUuid, - type Tx, - TxFactory, - type TxWorkspaceEvent, - type Version, - versionToString, - withContext, - type WorkspaceDataId, - WorkspaceEvent, - type WorkspaceIds, - type WorkspaceInfoWithStatus, - type WorkspaceUuid -} from '@hcengineering/core' -import { type Status, UNAUTHORIZED, unknownError } from '@hcengineering/platform' -import { - type HelloRequest, - type HelloResponse, - type RateLimitInfo, - type Request, - type Response, - SlidingWindowRateLimitter -} from '@hcengineering/rpc' -import { - type AddSessionResponse, - type ClientSessionCtx, - type ConnectionSocket, - type ConsumerHandle, - type GetWorkspaceResponse, - LOGGING_ENABLED, - pingConst, - type Pipeline, - type PipelineFactory, - type PlatformQueue, - type PlatformQueueProducer, - QueueTopic, - type QueueUserMessage, - QueueWorkspaceEvent, - type QueueWorkspaceMessage, - type Session, - type SessionHealth, - type SessionManager, - userEvents, - type UserStatistics, - workspaceEvents, - type WorkspaceStatistics -} from '@hcengineering/server-core' -import { generateToken, type Token } from '@hcengineering/server-token' -import { ClientSession } from './client' -import { sendResponse } from './utils' -import { Workspace } from './workspace' - -const ticksPerSecond = 20 -const workspaceSoftShutdownTicks = 15 * ticksPerSecond - -const guestAccount = 'b6996120-416f-49cd-841e-e4a5d2e49c9b' - -const hangRequestTimeoutSeconds = 30 -const hangSessionTimeoutSeconds = 60 - -/** - * @public - */ -export interface Timeouts { - // Timeout preferences - pingTimeout: number // Default 10 second - reconnectTimeout: number // Default 3 seconds -} - -export class TSessionManager implements SessionManager { - private readonly statusPromises = new Map>() - readonly workspaces = new Map() - checkInterval: any - - sessions = new Map() - reconnectIds = new Set() - - maintenanceTimer: any - timeMinutes = 0 - maintenanceMessage?: string - - modelVersion = process.env.MODEL_VERSION ?? '' - serverVersion = process.env.VERSION ?? '' - - workspaceProducer: PlatformQueueProducer - usersProducer: PlatformQueueProducer - workspaceConsumer: ConsumerHandle - - now: number = Date.now() - - ticksContext: MeasureContext - - hungSessionsWarnPercent = parseInt(process.env.HUNG_SESSIONS_WARN_PERCENT ?? '25') - hungSessionsFailPercent = parseInt(process.env.HUNG_SESSIONS_FAIL_PERCENT ?? '75') - hungRequestsFailPercent = parseInt(process.env.HUNG_REQUESTS_PERCENT ?? '50') - - constructor ( - readonly ctx: MeasureContext, - readonly timeouts: Timeouts, - readonly brandingMap: BrandingMap, - readonly profiling: - | { - start: () => void - stop: () => Promise - } - | undefined, - readonly accountsUrl: string, - readonly enableCompression: boolean, - readonly doHandleTick: boolean = true, - readonly queue: PlatformQueue, - readonly pipelineFactory: PipelineFactory - ) { - if (this.doHandleTick) { - this.checkInterval = setInterval(() => { - this.handleTick() - }, 1000 / ticksPerSecond) - } - this.workspaceProducer = this.queue.getProducer(ctx.newChild('ws-queue', {}, { span: false }), QueueTopic.Workspace) - this.usersProducer = this.queue.getProducer(ctx.newChild('user-queue', {}, { span: false }), QueueTopic.Users) - - this.workspaceConsumer = this.queue.createConsumer( - ctx.newChild('ws-queue-consume', {}, { span: false }), - QueueTopic.Workspace, - generateId(), - async (ctx, msg) => { - const m = msg.value - if ( - m.type === QueueWorkspaceEvent.Upgraded || - m.type === QueueWorkspaceEvent.Restored || - m.type === QueueWorkspaceEvent.Deleted - ) { - // Handle workspace messages - this.workspaceInfoCache.delete(msg.workspace) - } - } - ) - - this.ticksContext = ctx.newChild('ticks', {}, { span: false }) - } - - scheduleMaintenance (timeMinutes: number, message?: string): void { - this.timeMinutes = timeMinutes - this.maintenanceMessage = message - - this.sendMaintenanceWarning() - - const nextTime = (): number => (this.timeMinutes > 1 ? 60 * 1000 : this.timeMinutes * 60 * 1000) - - const showMaintenance = (): void => { - if (this.timeMinutes > 1) { - this.timeMinutes -= 1 - clearTimeout(this.maintenanceTimer) - this.maintenanceTimer = setTimeout(showMaintenance, nextTime()) - } else { - this.timeMinutes = 0 - } - - this.sendMaintenanceWarning() - } - - clearTimeout(this.maintenanceTimer) - this.maintenanceTimer = setTimeout(showMaintenance, nextTime()) - } - - private sendMaintenanceWarning (): void { - if (this.timeMinutes === 0) { - return - } - const event: TxWorkspaceEvent = this.createMaintenanceWarning() - for (const ws of this.workspaces.values()) { - this.doBroadcast(this.ctx, ws, [event]) - } - } - - private createMaintenanceWarning (): TxWorkspaceEvent { - return { - _id: generateId(), - _class: core.class.TxWorkspaceEvent, - event: WorkspaceEvent.MaintenanceNotification, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - objectSpace: core.space.DerivedTx, - space: core.space.DerivedTx, - createdBy: core.account.System, - params: { - timeMinutes: this.timeMinutes, - message: this.maintenanceMessage - } - } - } - - ticks = 0 - - handleTick (): void { - const now = Date.now() - this.handleWorkspaceTick() - - this.handleSessionTick(now) - this.ticks++ - } - - calcWorkspaceStats (sessions: { session: Session }[]): { sys: number, user: number, anonymous: number } { - let user: number = 0 - let sys: number = 0 - let anonymous: number = 0 - for (const s of sessions) { - if (s.session.getUser() === systemAccountUuid) { - sys++ - } else { - user++ - if (s.session.getUser() === guestAccount || s.session.getUser() === readOnlyGuestAccountUuid) { - anonymous++ - } - } - } - return { sys, user, anonymous } - } - - private handleWorkspaceTick (): void { - this.ctx.measure('sessions', this.sessions.size) - - const { sys, user, anonymous } = this.calcWorkspaceStats(Array.from(this.sessions.values())) - - let userWorkspaces: number = 0 - let sysOnlyWorkspaces: number = 0 - - for (const ws of this.workspaces.values()) { - const { sys, user } = this.calcWorkspaceStats(Array.from(ws.sessions.values())) - if (user > 0) { - userWorkspaces++ - } else { - if (sys > 0) { - sysOnlyWorkspaces++ - } - } - } - - this.ctx.measure('sessions-user', user) - this.ctx.measure('sessions-system', sys) - this.ctx.measure('sessions-anonymous', anonymous) - - this.ctx.measure('workspaces', this.workspaces.size) - this.ctx.measure('workspaces-user', userWorkspaces) - this.ctx.measure('workspaces-systemonly', sysOnlyWorkspaces) - - if (this.ticks % (60 * ticksPerSecond) === 0) { - const workspacesToUpdate: WorkspaceUuid[] = [] - - for (const [wsId, workspace] of this.workspaces.entries()) { - // update account lastVisit every minute per every workspace.∏ - for (const val of workspace.sessions.values()) { - if (val.session.getUser() !== systemAccountUuid) { - workspacesToUpdate.push(wsId) - break - } - } - } - if (workspacesToUpdate.length > 0) { - void this.updateLastVisit(this.ctx, workspacesToUpdate).catch(() => { - // Ignore - }) - } - } - for (const [wsId, workspace] of this.workspaces.entries()) { - for (const [k, v] of Array.from(workspace.tickHandlers.entries())) { - v.ticks-- - if (v.ticks === 0) { - workspace.tickHandlers.delete(k) - try { - v.operation() - } catch (err: any) { - Analytics.handleError(err) - } - } - } - - for (const s of workspace.sessions) { - if (this.ticks % (5 * 60 * ticksPerSecond) === workspace.tickHash) { - s[1].session.mins5.find = s[1].session.current.find - s[1].session.mins5.tx = s[1].session.current.tx - - s[1].session.current = { find: 0, tx: 0 } - } - } - - // Wait some time for new client to appear before closing workspace. - if ( - !workspace.maintenance && - workspace.sessions.size === 0 && - workspace.closing === undefined && - workspace.workspaceInitCompleted - ) { - workspace.softShutdown-- - if (workspace.softShutdown <= 0) { - this.ctx.warn('closing workspace, no users', { - workspace: workspace.wsId.url, - wsId, - upgrade: workspace.maintenance - }) - workspace.closing = this.performWorkspaceCloseCheck(workspace) - } - } else { - workspace.softShutdown = workspaceSoftShutdownTicks - } - } - } - - private handleSessionTick (now: number): void { - for (const s of this.sessions.values()) { - const isCurrentUserTick = this.ticks % ticksPerSecond === s.tickHash - - if (isCurrentUserTick) { - const wsId = s.session.workspace.uuid - const lastRequestDiff = now - s.session.lastRequest - - let timeout = hangSessionTimeoutSeconds * 1000 - if (s.session.getUser() === systemAccountUuid) { - timeout = timeout * 10 - } - if (lastRequestDiff > timeout) { - this.ctx.warn('session hang, closing...', { - wsId, - user: s.session.getUser() - }) - - // Force close workspace if only one client and it hang. - void this.close(this.ticksContext, s.socket, wsId).catch((err) => { - this.ctx.error('failed to close', err) - }) - continue - } - if ( - lastRequestDiff + (1 / 10) * lastRequestDiff > this.timeouts.pingTimeout && - now - s.session.lastPing > this.timeouts.pingTimeout - ) { - // We need to check state and close socket if it broken - // And ping other wize - s.session.lastPing = now - if (s.socket.checkState()) { - void s.socket.send(this.ticksContext, { result: pingConst }, s.session.binaryMode, s.session.useCompression) - } - } - for (const r of s.session.requests.values()) { - const sec = Math.round((now - r.start) / 1000) - if (sec > 0 && sec % hangRequestTimeoutSeconds === 0) { - this.ctx.warn('request hang found', { - sec, - wsId, - total: s.session.requests.size, - user: s.session.getUser(), - params: cutObjectArray(r.params) - }) - } - } - } - } - } - - createSession (token: Token, workspace: WorkspaceIds, info: LoginInfoWithWorkspaces): Session { - let primarySocialId: PersonId - let role: AccountRole = info.workspaces[workspace.uuid]?.role ?? AccountRole.User - switch (info.account) { - case systemAccountUuid: - primarySocialId = core.account.System - role = AccountRole.Owner - break - case guestAccount: - primarySocialId = '' as PersonId - role = AccountRole.DocGuest - break - default: - primarySocialId = pickPrimarySocialId(info.socialIds)._id - } - - return new ClientSession( - token, - workspace, - { - uuid: info.account, - socialIds: info.socialIds.map((it) => it._id), - primarySocialId, - fullSocialIds: info.socialIds, - role - }, - info, - token.extra?.mode === 'backup' - ) - } - - @withContext('🧭 get-workspace-info') - async getWorkspaceInfo ( - ctx: MeasureContext, - token: string, - updateLastVisit = true - ): Promise { - try { - return await getAccountClient(this.accountsUrl, token).getWorkspaceInfo(updateLastVisit) - } catch (err: any) { - if (err?.cause?.code === 'ECONNRESET' || err?.cause?.code === 'ECONNREFUSED') { - return undefined - } - throw err - } - } - - @withContext('🧭 update-last-visit') - async updateLastVisit (ctx: MeasureContext, workspaces: WorkspaceUuid[]): Promise { - try { - const sysToken = generateToken(systemAccountUuid, undefined, { service: 'transactor' }) - await getAccountClient(this.accountsUrl, sysToken).updateLastVisit(workspaces) - } catch (err: any) { - if (err?.cause?.code === 'ECONNRESET' || err?.cause?.code === 'ECONNREFUSED') { - return undefined - } - throw err - } - } - - @withContext('🧭 get-login-with-workspace-info') - async getLoginWithWorkspaceInfo (ctx: MeasureContext, token: string): Promise { - try { - const accountClient = getAccountClient(this.accountsUrl, token) - return await accountClient.getLoginWithWorkspaceInfo() - } catch (err: any) { - if (err?.cause?.code === 'ECONNRESET' || err?.cause?.code === 'ECONNREFUSED') { - return undefined - } - throw err - } - } - - countUserSessions (workspace: Workspace, accountUuid: AccountUuid): number { - return Array.from(workspace.sessions.values()) - .filter((it) => it.session.getUser() === accountUuid) - .reduce((acc) => acc + 1, 0) - } - - checkHealth (): SessionHealth { - const now = Date.now() - - let totalSessions = 0 - let hungSessions = 0 - - for (const [, { session }] of this.sessions) { - let totalRequests = 0 - let hungRequests = 0 - - // Ignore system account sessions - if (session.getUser() === systemAccountUuid) { - continue - } - - totalSessions += 1 - - // Check if the session is hung - const lastRequestDiff = now - session.lastRequest - if (lastRequestDiff > hangSessionTimeoutSeconds * 1000) { - hungSessions += 1 - continue - } - - // Check if requests are hung - for (const r of session.requests.values()) { - const sec = Math.round((now - r.start) / 1000) - if (sec > hangRequestTimeoutSeconds) { - hungRequests += 1 - } - totalRequests += 1 - } - - const hungRequestsPercent = totalRequests > 0 ? (100 * hungRequests) / totalRequests : 0 - if (hungRequestsPercent > this.hungRequestsFailPercent) { - hungSessions += 1 - } - } - - this.ctx.measure('sessions-hung', hungSessions) - - const hungSessionsPercent = totalSessions > 0 ? (100 * hungSessions) / totalSessions : 0 - - if (hungSessionsPercent > this.hungSessionsFailPercent) { - this.ctx.warn('high hung sessions', { hungSessionsPercent }) - return 'unhealthy' - } - - if (hungSessionsPercent > this.hungSessionsWarnPercent) { - this.ctx.warn('high degraded sessions', { hungSessionsPercent }) - return 'degraded' - } - - return 'healthy' - } - - tickCounter = 0 - - @withContext('🧭 get-workspace') - async getWorkspace ( - ctx: MeasureContext, - workspaceUuid: WorkspaceUuid, - workspaceInfo: LoginInfoWorkspace | undefined, - token: Token, - ws: ConnectionSocket - ): Promise<{ workspace?: Workspace, resp?: GetWorkspaceResponse }> { - if (workspaceInfo === undefined) { - return { resp: { error: new Error('Workspace not found or not available'), terminate: true } } - } - - if (isArchivingMode(workspaceInfo.mode)) { - // No access to disabled workspaces for regular users - return { resp: { error: new Error('Workspace is archived'), terminate: true, specialError: 'archived' } } - } - if (isMigrationMode(workspaceInfo.mode)) { - // No access to disabled workspaces for regular users - return { - resp: { error: new Error('Workspace is in region migration'), terminate: true, specialError: 'migration' } - } - } - if (isRestoringMode(workspaceInfo.mode)) { - // No access to disabled workspaces for regular users - return { - resp: { error: new Error('Workspace is in backup restore'), terminate: true, specialError: 'migration' } - } - } - - if (isWorkspaceCreating(workspaceInfo.mode)) { - // No access to workspace for token. - return { resp: { error: new Error(`Workspace during creation phase...${workspaceUuid}`) } } - } - - const wsVersion: Data = { - major: workspaceInfo.version.versionMajor, - minor: workspaceInfo.version.versionMinor, - patch: workspaceInfo.version.versionPatch - } - - if ( - this.modelVersion !== '' && - this.modelVersion !== versionToString(wsVersion) && - token.extra?.model !== 'upgrade' && - token.extra?.mode !== 'backup' - ) { - ctx.warn('Model version mismatch', { - source: token.extra?.service ?? 'user', - version: this.modelVersion, - workspaceVersion: versionToString(wsVersion), - workspace: workspaceUuid - }) - // Version mismatch, return upgrading. - return { resp: { upgrade: true, progress: workspaceInfo.mode === 'upgrading' ? workspaceInfo.progress ?? 0 : 0 } } - } - - let workspace = this.workspaces.get(workspaceUuid) - if (workspace?.closing !== undefined) { - await workspace?.closing - } - - workspace = this.workspaces.get(workspaceUuid) - - const branding = - (workspaceInfo.branding !== undefined - ? Object.values(this.brandingMap).find((b) => b.key === workspaceInfo?.branding) - : null) ?? null - - if (workspace === undefined) { - ctx.warn('open workspace', { - account: token.account, - workspace: workspaceUuid, - ...token.extra - }) - - workspace = this.createWorkspace(ctx.parent ?? ctx, ctx, token, workspaceInfo.url, workspaceInfo.dataId, branding) - await this.workspaceProducer.send(ctx, workspaceUuid, [workspaceEvents.open()]) - } - - if (token.extra?.model === 'upgrade') { - if (workspace.maintenance) { - ctx.warn('reconnect workspace in upgrade', { - account: token.account, - workspace: workspaceUuid, - wsUrl: workspaceInfo.url - }) - } else { - ctx.warn('reconnect workspace in upgrade switch', { - email: token.account, - workspace: workspaceUuid, - wsUrl: workspaceInfo.url - }) - - // We need to wait in case previous upgrade connection is already closing. - await this.switchToUpgradeSession(token, ctx.parent ?? ctx, workspace, ws) - } - } else { - if (workspace.maintenance || this.maintenanceWorkspaces.has(workspace.wsId.uuid)) { - ctx.warn('connect during upgrade', { - account: token.account, - workspace: workspace.wsId.url, - sessionUsers: Array.from(workspace.sessions.values()).map((it) => it.session.getUser()), - sessionData: Array.from(workspace.sessions.values()).map((it) => it.socket.data()) - }) - - return { resp: { upgrade: true } } - } - } - return { workspace } - } - - sysAccount = { - account: systemAccountUuid, - name: 'System', - workspaces: {}, - socialIds: [] - } - - maintenanceWorkspaces = new Set() - - workspaceInfoCache = new Map() - - async addSession ( - ctx: MeasureContext, - ws: ConnectionSocket, - token: Token, - rawToken: string, - sessionId: string | undefined - ): Promise { - return await ctx.with('📲 add-session', { source: token.extra?.service ?? '🤦‍♂️user' }, async (ctx) => { - let account: LoginInfoWithWorkspaces | undefined - - try { - if (token.account === undefined) { - return { error: UNAUTHORIZED, terminate: true } - } - account = - token.account === systemAccountUuid ? this.sysAccount : await this.getLoginWithWorkspaceInfo(ctx, rawToken) - } catch (err: any) { - return { error: err } - } - - if (account === undefined) { - return { error: new Error('Account not found or not available'), terminate: true } - } - - let wsInfo = account.workspaces[token.workspace] - - if (wsInfo === undefined) { - // In case of guest or system account - // We need to get workspace info for system account. - const workspaceInfo = - this.workspaceInfoCache.get(token.workspace) ?? (await this.getWorkspaceInfo(ctx, rawToken, false)) - if (workspaceInfo === undefined) { - return { error: new Error('Workspace not found or not available'), terminate: true } - } - this.workspaceInfoCache.set(token.workspace, workspaceInfo) - - wsInfo = { - url: workspaceInfo.url, - mode: workspaceInfo.mode, - dataId: workspaceInfo.dataId, - version: { - versionMajor: workspaceInfo.versionMajor, - versionMinor: workspaceInfo.versionMinor, - versionPatch: workspaceInfo.versionPatch - }, - role: AccountRole.Owner, - endpoint: { externalUrl: '', internalUrl: '', region: workspaceInfo.region ?? '' }, - progress: workspaceInfo.processingProgress, - branding: workspaceInfo.branding - } - } else { - this.workspaceInfoCache.delete(token.workspace) - } - const { workspace, resp } = await this.getWorkspace(ctx.parent ?? ctx, token.workspace, wsInfo, token, ws) - if (resp !== undefined) { - return resp - } - - if (workspace === undefined || account === undefined) { - // Should not happen - return { error: new Error('Workspace not found or not available'), terminate: true } - } - - const oldSession = sessionId !== undefined ? workspace.sessions?.get(sessionId) : undefined - if (oldSession !== undefined) { - // Just close old socket for old session id. - await this.close(ctx, oldSession.socket, workspace.wsId.uuid) - } - - const session = this.createSession(token, workspace.wsId, account) - - session.sessionId = sessionId !== undefined && (sessionId ?? '').trim().length > 0 ? sessionId : generateId() - session.sessionInstanceId = generateId() - const tickHash = this.tickCounter % ticksPerSecond - - this.sessions.set(ws.id, { session, socket: ws, tickHash }) - // We need to delete previous session with Id if found. - this.tickCounter++ - workspace.sessions.set(session.sessionId, { session, socket: ws, tickHash }) - - const accountUuid = account.account - if (accountUuid !== systemAccountUuid && accountUuid !== guestAccount) { - await this.usersProducer.send(ctx, workspace.wsId.uuid, [ - userEvents.login({ - user: accountUuid, - sessions: this.countUserSessions(workspace, accountUuid), - socialIds: account.socialIds.map((it) => it._id) - }) - ]) - } - - // Mark workspace as init completed and we had at least one client. - if (!workspace.workspaceInitCompleted) { - workspace.workspaceInitCompleted = true - } - - if (this.timeMinutes > 0) { - void ws - .send(ctx, { result: this.createMaintenanceWarning() }, session.binaryMode, session.useCompression) - .catch((err) => { - ctx.error('failed to send maintenance warning', err) - }) - } - return { session, context: workspace.context, workspaceId: workspace.wsId.uuid } - }) - } - - private async switchToUpgradeSession ( - token: Token, - ctx: MeasureContext, - workspace: Workspace, - ws: ConnectionSocket - ): Promise { - if (LOGGING_ENABLED) { - ctx.info('reloading workspace', { url: workspace.wsId.url, token: JSON.stringify(token) }) - } - - // Mark as upgrade, to prevent any new clients to connect during close - workspace.maintenance = true - // If upgrade client is used. - // Drop all existing clients - await this.doCloseAll(workspace, 0, 'upgrade', ws) - } - - broadcastAll ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - tx: Tx[], - target?: AccountUuid | AccountUuid[], - exclude?: AccountUuid[] - ): void { - const ws = this.workspaces.get(workspace) - if (ws === undefined) { - return - } - this.doBroadcast(ctx, ws, tx, target, exclude) - } - - doBroadcast ( - ctx: MeasureContext, - ws: Workspace, - tx: Tx[], - target?: AccountUuid | AccountUuid[], - exclude?: AccountUuid[] - ): void { - if (ws.maintenance) { - return - } - if (target !== undefined && !Array.isArray(target)) { - target = [target] - } - ctx = ctx.newChild('📬 broadcast-all', {}) - const sessions = [...ws.sessions.values()].filter((it) => { - if (it === undefined) { - return false - } - const tt = it.session.getUser() - return (target === undefined && !(exclude ?? []).includes(tt)) || (target?.includes(tt) ?? false) - }) - function send (): void { - const promises: Promise[] = [] - for (const session of sessions) { - try { - promises.push( - sendResponse(ctx, session.session, session.socket, { result: tx }).catch((err) => { - ctx.error('failed to send', err) - }) - ) - } catch (err: any) { - Analytics.handleError(err) - ctx.error('error during send', { error: err }) - } - } - void Promise.all(promises).finally(() => { - ctx.end() - }) - } - if (sessions.length > 0) { - // We need to send broadcast after our client response so put it after all IO - send() - } else { - ctx.end() - } - } - - broadcastSessions (measure: MeasureContext, sessionIds: Record): void { - const ctx = measure.newChild('📬 broadcast sessions', {}) - const allSessions = Array.from(this.sessions.values()) - const sessions = Object.entries(sessionIds).map(([sessionId, txes]) => ({ - session: allSessions.find((it) => it.session.sessionId === sessionId), - txes - })) - - function send (): void { - for (const session of sessions) { - if (session.session === undefined) { - continue - } - try { - void sendResponse(ctx, session.session.session, session.session.socket, { result: session.txes }) - } catch (err: any) { - Analytics.handleError(err) - ctx.error('error during send', { error: err }) - } - } - ctx.end() - } - if (sessions.length > 0) { - // We need to send broadcast after our client response so put it after all IO - send() - } else { - ctx.end() - } - } - - broadcast ( - ctx: MeasureContext, - from: Session | null, - workspaceId: WorkspaceUuid, - resp: Tx[], - target: AccountUuid | undefined, - exclude?: AccountUuid[] - ): void { - const workspace = this.workspaces.get(workspaceId) - if (workspace === undefined) { - this.ctx.error('internal: cannot find sessions', { - workspaceId, - target, - userId: from?.getUser() ?? '$unknown' - }) - return - } - if (workspace?.maintenance ?? false) { - return - } - - const sessions = [...workspace.sessions.values()] - ctx = ctx.newChild('📭 broadcast', {}) - const send = (): void => { - for (const sessionRef of sessions) { - const tt = sessionRef.session.getUser() - if ((target === undefined && !(exclude ?? []).includes(tt)) || (target?.includes(tt) ?? false)) { - sessionRef.session.broadcast(ctx, sessionRef.socket, resp) - } - } - ctx.end() - } - if (sessions.length > 0) { - // We need to send broadcast after our client response so put it after all IO - send() - } else { - ctx.end() - } - } - - private createWorkspace ( - ctx: MeasureContext, - pipelineCtx: MeasureContext, - token: Token, - workspaceUrl: string, - workspaceDataId: WorkspaceDataId | undefined, - branding: Branding | null - ): Workspace { - const upgrade = token.extra?.model === 'upgrade' - const context = ctx.newChild('🧲 session', {}, { span: false }) - const workspaceIds: WorkspaceIds = { - uuid: token.workspace, - dataId: workspaceDataId, - url: workspaceUrl - } - - const factory = async (): Promise => { - const pipeline = await this.pipelineFactory( - pipelineCtx, - workspaceIds, - { - broadcast: (ctx, tx, targets, exclude) => { - this.broadcastAll(ctx, workspaceIds.uuid, tx, targets, exclude) - }, - broadcastSessions: (ctx, sessions) => { - this.broadcastSessions(ctx, sessions) - } - }, - branding - ) - return pipeline - } - const workspace: Workspace = new Workspace( - context, - generateToken(systemAccountUuid, token.workspace, { service: 'transactor' }), - factory, - this.tickCounter % ticksPerSecond, - workspaceSoftShutdownTicks, - workspaceIds, - branding - ) - workspace.maintenance = upgrade - this.workspaces.set(token.workspace, workspace) - - return workspace - } - - private async trySetStatus ( - ctx: MeasureContext, - pipeline: Pipeline, - session: Session, - online: boolean, - workspaceId: WorkspaceUuid - ): Promise { - const current = this.statusPromises.get(session.getUser()) - if (current !== undefined) { - await current - } - const promise = this.setStatus(ctx, pipeline, session, online, workspaceId) - this.statusPromises.set(session.getUser(), promise) - await promise - this.statusPromises.delete(session.getUser()) - } - - private async setStatus ( - ctx: MeasureContext, - pipeline: Pipeline, - session: Session, - online: boolean, - workspaceId: WorkspaceUuid - ): Promise { - try { - const user = session.getUser() - const userRawAccount = session.getRawAccount() - if (user === undefined || userRawAccount.role === AccountRole.ReadOnlyGuest) return - - const clientCtx: ClientSessionCtx = { - requestId: undefined, - pipeline, - sendResponse: async () => { - // No response - }, - ctx, - socialStringsToUsers: this.getActiveSocialStringsToUsersMap(workspaceId, session), - sendError: async () => { - // Assume no error send - }, - sendPong: () => {} - } - - const status = (await session.findAllRaw(clientCtx, core.class.UserStatus, { user }, { limit: 1 }))[0] - const txFactory = new TxFactory(userRawAccount.primarySocialId, true) - if (status === undefined) { - const tx = txFactory.createTxCreateDoc(core.class.UserStatus, core.space.Space, { - online, - user - }) - await session.tx(clientCtx, tx) - } else if (status.online !== online) { - const tx = txFactory.createTxUpdateDoc(status._class, status.space, status._id, { - online - }) - await session.tx(clientCtx, tx) - } - } catch (err: any) { - ctx.error('failed to set status', { err }) - Analytics.handleError(err) - } - } - - async close (ctx: MeasureContext, ws: ConnectionSocket, workspaceUuid: WorkspaceUuid): Promise { - const workspace = this.workspaces.get(workspaceUuid) - - const sessionRef = this.sessions.get(ws.id) - if (sessionRef !== undefined) { - ctx.info('bye happen', { - workspaceId: workspace?.wsId.uuid, - userId: sessionRef.session.getUser(), - user: sessionRef.session.getSocialIds().find((it) => it.type !== SocialIdType.HULY)?.value, - binary: sessionRef.session.binaryMode, - compression: sessionRef.session.useCompression, - totalTime: this.now - sessionRef.session.createTime, - workspaceUsers: workspace?.sessions?.size, - totalUsers: this.sessions.size - }) - this.sessions.delete(ws.id) - - if (workspace !== undefined) { - workspace.sessions.delete(sessionRef.session.sessionId) - - const userUuid = sessionRef.session.getUser() - await this.usersProducer.send(ctx, workspaceUuid, [ - userEvents.logout({ - user: userUuid, - sessions: this.countUserSessions(workspace, userUuid), - socialIds: sessionRef.session.getUserSocialIds() - }) - ]) - - if (this.doHandleTick) { - workspace.tickHandlers.set(sessionRef.session.sessionId, { - ticks: this.timeouts.reconnectTimeout * ticksPerSecond, - operation: () => { - this.reconnectIds.delete(sessionRef.session.sessionId) - const user = sessionRef.session.getUser() - if (workspace !== undefined) { - const another = Array.from(workspace.sessions.values()).findIndex((p) => p.session.getUser() === user) - if (another === -1 && !workspace.maintenance) { - void workspace.with(async (pipeline) => { - await pipeline.closeSession(ctx, sessionRef.session.sessionId) - // await communicationApi.closeSession(sessionRef.session.sessionId) - if (user !== guestAccount && user !== systemAccountUuid) { - await this.trySetStatus( - workspace.context.newChild('status', {}), - pipeline, - sessionRef.session, - false, - workspaceUuid - ).catch(() => {}) - } - }) - } - } - } - }) - } - this.reconnectIds.add(sessionRef.session.sessionId) - } - try { - sessionRef.socket.close() - } catch (err) { - // Ignore if closed - } - } - } - - async forceMaintenance (ctx: MeasureContext, workspaceId: WorkspaceUuid): Promise { - const workspace = this.workspaces.get(workspaceId) - this.maintenanceWorkspaces.add(workspaceId) - if (workspace !== undefined) { - workspace.maintenance = true - ctx.info('force-maintenance', { workspaceId }) - await this.doCloseAll(workspace, 99, 'force-close') - } - } - - async forceClose (wsId: WorkspaceUuid, ignoreSocket?: ConnectionSocket): Promise { - const ws = this.workspaces.get(wsId) - this.maintenanceWorkspaces.delete(wsId) - this.workspaceInfoCache.delete(wsId) - if (ws !== undefined) { - this.ctx.warn('force-close', { name: ws.wsId.url }) - ws.maintenance = true // We need to similare upgrade to refresh all clients. - ws.closing = this.doCloseAll(ws, 99, 'force-close', ignoreSocket) - this.workspaces.delete(wsId) - await ws.closing - ws.closing = undefined - } else { - this.ctx.warn('force-close-unknown', { wsId }) - } - } - - async doCloseAll ( - workspace: Workspace, - code: number, - reason: 'upgrade' | 'shutdown' | 'force-close', - ignoreSocket?: ConnectionSocket - ): Promise { - if (LOGGING_ENABLED) { - this.ctx.info('closing workspace', { - url: workspace.wsId.url, - uuid: workspace.wsId.uuid, - code, - reason - }) - } - - const sessions = Array.from(workspace.sessions) - workspace.sessions.clear() - - const closeS = (s: Session, webSocket: ConnectionSocket): void => { - s.workspaceClosed = true - if (reason === 'upgrade' || reason === 'force-close') { - // Override message handler, to wait for upgrading response from clients. - this.sendUpgrade(workspace.context, webSocket, s.binaryMode, s.useCompression) - } - webSocket.close() - this.reconnectIds.delete(s.sessionId) - } - - if (LOGGING_ENABLED) { - this.ctx.warn('Clients disconnected. Closing Workspace...', { - url: workspace.wsId.url, - uuid: workspace.wsId.uuid - }) - } - - sessions - .filter((it) => it[1].socket.id !== ignoreSocket?.id) - .forEach((s) => { - closeS(s[1].session, s[1].socket) - }) - - if (reason !== 'upgrade') { - await workspace.close(this.ctx) - if (LOGGING_ENABLED) { - this.ctx.warn('Workspace closed...', { uuid: workspace.wsId.uuid, url: workspace.wsId.url }) - } - } - } - - private sendUpgrade (ctx: MeasureContext, webSocket: ConnectionSocket, binary: boolean, compression: boolean): void { - void webSocket.send( - ctx, - { - result: { - _class: core.class.TxModelUpgrade - } - }, - binary, - compression - ) - } - - async closeWorkspaces (ctx: MeasureContext): Promise { - clearInterval(this.checkInterval) - for (const w of this.workspaces) { - await this.doCloseAll(w[1], 1, 'shutdown') - } - await this.workspaceProducer.close() - await this.usersProducer.close() - } - - private async performWorkspaceCloseCheck (workspace: Workspace): Promise { - const uuid = workspace.wsId.uuid - const logParams = { uuid, url: workspace.wsId.url } - if (workspace.sessions.size === 0) { - if (LOGGING_ENABLED) { - this.ctx.warn('no sessions for workspace', logParams) - } - try { - if (workspace.sessions.size === 0) { - await workspace.close(this.ctx) - - this.workspaces.delete(uuid) - workspace.context.end() - if (LOGGING_ENABLED) { - this.ctx.warn('Closed workspace', logParams) - } - - await this.workspaceProducer.send(this.ctx, workspace.wsId.uuid, [workspaceEvents.down()]) - } - } catch (err: any) { - Analytics.handleError(err) - this.workspaces.delete(uuid) - if (LOGGING_ENABLED) { - this.ctx.error('failed', { ...logParams, error: err }) - } - } - } else { - if (LOGGING_ENABLED) { - this.ctx.info('few sessions for workspace, close skipped', { - ...logParams, - sessions: workspace.sessions.size - }) - } - } - } - - createOpContext ( - ctx: MeasureContext, - sendCtx: MeasureContext, - pipeline: Pipeline, - requestId: Request['id'], - service: Session, - ws: ConnectionSocket, - rateLimit: RateLimitInfo | undefined - ): ClientSessionCtx { - const st = platformNow() - return { - ctx, - pipeline, - requestId, - sendResponse: (reqId, msg) => - sendResponse(sendCtx, service, ws, { - id: reqId, - result: msg, - time: platformNowDiff(st), - bfst: this.now, - queue: service.requests.size, - rateLimit - }), - sendPong: () => { - ws.sendPong() - }, - socialStringsToUsers: this.getActiveSocialStringsToUsersMap(service.workspace.uuid), - sendError: (reqId, msg, error: Status) => - sendResponse(sendCtx, service, ws, { - id: reqId, - result: msg, - error, - time: platformNowDiff(st), - rateLimit, - bfst: this.now, - queue: service.requests.size - }) - } - } - - // TODO: cache this map and update when sessions created/closed - getActiveSocialStringsToUsersMap ( - workspace: WorkspaceUuid, - ...extra: Session[] - ): Map< - PersonId, - { - accontUuid: AccountUuid - role: AccountRole - } - > { - const ws = this.workspaces.get(workspace) - if (ws === undefined) { - return new Map() - } - const res = new Map< - PersonId, - { - accontUuid: AccountUuid - role: AccountRole - } - >() - for (const s of [...Array.from(ws.sessions.values()).map((it) => it.session), ...extra]) { - const sessionAccount = s.getUser() - if (sessionAccount === systemAccountUuid) { - continue - } - const userSocialIds = s.getUserSocialIds() - for (const id of userSocialIds) { - res.set(id, { - accontUuid: sessionAccount, - role: s.getRawAccount().role - }) - } - } - return res - } - - limitter = new SlidingWindowRateLimitter( - parseInt(process.env.RATE_LIMIT_MAX ?? '1500'), - parseInt(process.env.RATE_LIMIT_WINDOW ?? '30000'), - () => Date.now() - ) - - sysLimitter = new SlidingWindowRateLimitter( - parseInt(process.env.RATE_LIMIT_MAX ?? '5000'), - parseInt(process.env.RATE_LIMIT_WINDOW ?? '30000'), - () => Date.now() - ) - - checkRate (service: Session): RateLimitInfo { - if (service.getUser() === systemAccountUuid) { - return this.sysLimitter.checkRateLimit('#sys#' + (service.token.extra?.service ?? '') + service.workspace.uuid) - } - return this.limitter.checkRateLimit(service.getUser() + (service.token.extra?.service ?? '')) - } - - async handleRequest( - requestCtx: MeasureContext, - service: S, - ws: ConnectionSocket, - request: Request, - workspaceId: WorkspaceUuid - ): Promise { - // Calculate total number of clients - const reqId = generateId() - const source = service.token.extra?.service ?? '🤦‍♂️user' - - const st = Date.now() - try { - if (request.time != null) { - const delta = Date.now() - request.time - requestCtx.measure('msg-receive-delta', delta) - } - const workspace = this.workspaces.get(workspaceId) - if (workspace === undefined || workspace.closing !== undefined) { - await ws.send( - requestCtx, - { - id: request.id, - error: unknownError('Workspace is closing') - }, - service.binaryMode, - service.useCompression - ) - return - } - if (request.id === -1 && request.method === 'hello') { - await requestCtx.with('🧨 handleHello', { source }, (ctx) => - this.handleHello(request, service, ctx, workspace, ws, requestCtx) - ) - return - } - if (request.id === -2 && request.method === 'forceClose') { - // TODO: we chould allow this only for admin or system accounts - let done = false - const wsRef = this.workspaces.get(workspaceId) - if (wsRef?.maintenance ?? false) { - done = true - this.ctx.warn('FORCE CLOSE', { workspace: workspaceId }) - // In case of upgrade, we need to force close workspace not in interval handler - await this.forceClose(workspaceId, ws) - } - const forceCloseResponse: Response = { - id: request.id, - result: done - } - await ws.send(requestCtx, forceCloseResponse, service.binaryMode, service.useCompression) - return - } - let rateLimit: RateLimitInfo | undefined - if (request.method !== 'ping') { - rateLimit = this.checkRate(service) - // If remaining is 0, rate limit is exceeded - if (rateLimit?.remaining === 0) { - service.updateLast() - void ws.send( - requestCtx, - { - id: request.id, - rateLimit, - error: unknownError('Rate limit') - }, - service.binaryMode, - service.useCompression - ) - return - } - } - - if (request.id === -1 && request.method === '#upgrade') { - ws.close() - return - } - service.requests.set(reqId, { - id: reqId, - params: request, - start: st - }) - - const f = (service as any)[request.method] - try { - const params = [...request.params] - - if (ws.isBackpressure()) { - await ws.backpressure(requestCtx) - } - - if (request.method === 'ping') { - service.lastRequest = Date.now() - ws.sendPong() - return - } - await workspace.with(async (pipeline) => { - await requestCtx.with( - '🧨' + request.method, - { source, mode: 'websocket' }, - (callTx) => - f.apply(service, [ - this.createOpContext(callTx, requestCtx, pipeline, request.id, service, ws, rateLimit), - ...params - ]), - { - user: service.getUser(), - socialId: service.getRawAccount().primarySocialId, - workspace: workspace.wsId.uuid - }, - { meta: request.meta } - ) - }) - } catch (err: any) { - Analytics.handleError(err) - if (LOGGING_ENABLED) { - this.ctx.error('error handle request', { error: err, request }) - } - await ws.send( - requestCtx, - { - id: request.id, - error: unknownError(err), - result: JSON.parse(JSON.stringify(err?.stack)) - }, - service.binaryMode, - service.useCompression - ) - } - } finally { - service.requests.delete(reqId) - } - } - - async handleRPC( - requestCtx: MeasureContext, - service: S, - method: string, - ws: ConnectionSocket, - operation: (ctx: ClientSessionCtx, rateLimit: RateLimitInfo | undefined) => Promise - ): Promise { - const rateLimitStatus = this.checkRate(service) - // If remaining is 0, rate limit is exceeded - if (rateLimitStatus?.remaining === 0) { - return await Promise.resolve(rateLimitStatus) - } - - const source = service.token.extra?.service ?? '🤦‍♂️user' - - // Calculate total number of clients - const reqId = generateId() - - const st = Date.now() - try { - const workspace = this.workspaces.get(service.workspace.uuid) - if (workspace === undefined || workspace.closing !== undefined) { - throw new Error('Workspace is closing') - } - - service.requests.set(reqId, { - id: reqId, - params: {}, - start: st - }) - - try { - await workspace.with(async (pipeline) => { - await requestCtx.with( - '🧨 ' + method, - { source, mode: 'rpc' }, - (callTx) => - operation( - this.createOpContext(callTx, requestCtx, pipeline, reqId, service, ws, rateLimitStatus), - rateLimitStatus - ), - { - user: service.getUser(), - socialId: service.getRawAccount().primarySocialId, - workspace: workspace.wsId.uuid - } - ) - }) - } catch (err: any) { - Analytics.handleError(err) - if (LOGGING_ENABLED) { - this.ctx.error('error handle request', { error: err }) - } - await ws.send( - requestCtx, - { - id: reqId, - error: unknownError(err), - result: JSON.parse(JSON.stringify(err?.stack)) - }, - service.binaryMode, - service.useCompression - ) - throw err - } - return undefined - } finally { - service.requests.delete(reqId) - } - } - - entryToUserStats = (session: Session, socket: ConnectionSocket): UserStatistics => { - return { - current: session.current, - mins5: session.mins5, - userId: session.getUser(), - sessionId: socket.id, - total: session.total, - data: socket.data - } - } - - workspaceToWorkspaceStats = (ws: Workspace): WorkspaceStatistics => { - return { - clientsTotal: new Set(Array.from(ws.sessions.values()).map((it) => it.session.getUser())).size, - sessionsTotal: ws.sessions.size, - workspaceName: ws.wsId.url, - wsId: ws.wsId.uuid, - sessions: Array.from(ws.sessions.values()).map((it) => this.entryToUserStats(it.session, it.socket)) - } - } - - getStatistics (): WorkspaceStatistics[] { - return Array.from(this.workspaces.values()).map((it) => this.workspaceToWorkspaceStats(it)) - } - - private async handleHello( - request: Request, - service: S, - ctx: MeasureContext, - workspace: Workspace, - ws: ConnectionSocket, - requestCtx: MeasureContext - ): Promise { - try { - const hello = request as HelloRequest - service.binaryMode = hello.binary ?? false - service.useCompression = this.enableCompression ? hello.compression ?? false : false - - if (LOGGING_ENABLED) { - ctx.info('hello happen', { - workspace: workspace.wsId.url, - workspaceId: workspace.wsId.uuid, - userId: service.getUser(), - user: service.getSocialIds().find((it) => it.type !== SocialIdType.HULY)?.value, - binary: service.binaryMode, - compression: service.useCompression, - timeToHello: Date.now() - service.createTime, - workspaceUsers: workspace.sessions.size, - totalUsers: this.sessions.size - }) - } - const reconnect = this.reconnectIds.has(service.sessionId) - if (reconnect) { - this.reconnectIds.delete(service.sessionId) - } - - const account = service.getRawAccount() - await workspace.with(async (pipeline) => { - const helloResponse: HelloResponse = { - id: -1, - result: 'hello', - binary: service.binaryMode, - reconnect, - serverVersion: this.serverVersion, - lastTx: pipeline.context.lastTx, - lastHash: pipeline.context.lastHash, - account, - useCompression: service.useCompression - } - await ws.send(ctx, helloResponse, false, false) - }) - if (account.uuid !== guestAccount && account.uuid !== systemAccountUuid) { - void workspace.with(async (pipeline) => { - // We do not need to wait for set-status, just return session to client - await workspace.context - .with('🧨 status', {}, (ctx) => this.trySetStatus(ctx, pipeline, service, true, service.workspace.uuid)) - .catch(() => {}) - }) - } - } catch (err: any) { - ctx.error('error', { err }) - } - } -} - -export function createSessionManager ( - ctx: MeasureContext, - brandingMap: BrandingMap, - timeouts: Timeouts, - profiling: - | { - start: () => void - stop: () => Promise - } - | undefined, - accountsUrl: string, - enableCompression: boolean, - doHandleTick: boolean = true, - queue: PlatformQueue, - pipelineFactory: PipelineFactory -): SessionManager { - return new TSessionManager( - ctx, - timeouts, - brandingMap ?? null, - profiling, - accountsUrl, - enableCompression, - doHandleTick, - queue, - pipelineFactory - ) -} - -export interface SessionManagerOptions extends Partial { - pipelineFactory: PipelineFactory - brandingMap: BrandingMap - enableCompression?: boolean - accountsUrl: string - profiling?: { - start: () => void - stop: () => Promise - } - queue: PlatformQueue -} - -/** - * @public - */ -export function startSessionManager (ctx: MeasureContext, opt: SessionManagerOptions): SessionManager { - const sessions = createSessionManager( - ctx, - opt.brandingMap, - { - pingTimeout: opt.pingTimeout ?? 10000, - reconnectTimeout: 30 // seconds to reconnect - }, - opt.profiling, - opt.accountsUrl, - opt.enableCompression ?? false, - true, - opt.queue, - opt.pipelineFactory - ) - return sessions -} diff --git a/server/server/src/starter.ts b/server/server/src/starter.ts deleted file mode 100644 index de682be0e64..00000000000 --- a/server/server/src/starter.ts +++ /dev/null @@ -1,75 +0,0 @@ -export interface ServerEnv { - dbUrl: string - mongoUrl?: string - fulltextUrl: string - serverSecret: string - frontUrl: string - filesUrl: string | undefined - mailUrl: string | undefined - mailAuthToken: string | undefined - webPushUrl: string | undefined - accountsUrl: string - serverPort: number - enableCompression: boolean - brandingPath: string | undefined -} - -export function serverConfigFromEnv (): ServerEnv { - const serverPort = parseInt(process.env.SERVER_PORT ?? '3333') - const enableCompression = (process.env.ENABLE_COMPRESSION ?? 'false') === 'true' - - const dbUrl = process.env.DB_URL - if (dbUrl === undefined) { - console.error('please provide DB_URL') - process.exit(1) - } - - const mongoUrl = process.env.MONGO_URL - - const fulltextUrl = process.env.FULLTEXT_URL - if (fulltextUrl === undefined) { - console.error('please provide Fulltext URL') - process.exit(1) - } - - const serverSecret = process.env.SERVER_SECRET - if (serverSecret === undefined) { - console.log('Please provide server secret') - process.exit(1) - } - - const frontUrl = process.env.FRONT_URL - if (frontUrl === undefined) { - console.log('Please provide FRONT_URL url') - process.exit(1) - } - - const filesUrl = process.env.FILES_URL - const mailUrl = process.env.MAIL_URL - const mailAuthToken = process.env.MAIL_AUTH_TOKEN - const webPushUrl = process.env.WEB_PUSH_URL - - const accountsUrl = process.env.ACCOUNTS_URL - if (accountsUrl === undefined) { - console.log('Please provide ACCOUNTS_URL url') - process.exit(1) - } - - const brandingPath = process.env.BRANDING_PATH - - return { - dbUrl, - mongoUrl, - fulltextUrl, - serverSecret, - frontUrl, - filesUrl, - mailUrl, - mailAuthToken, - webPushUrl, - accountsUrl, - serverPort, - enableCompression, - brandingPath - } -} diff --git a/server/server/src/stats.ts b/server/server/src/stats.ts deleted file mode 100644 index 28211dda194..00000000000 --- a/server/server/src/stats.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - type MeasureContext, - MeasureMetricsContext, - type Metrics, - metricsAggregate, - type MetricsData -} from '@hcengineering/core' -import { type SessionManager } from '@hcengineering/server-core' -import os from 'node:os' - -/** - * @public - */ -export function getStatistics (ctx: MeasureContext, sessions: SessionManager, admin: boolean): any { - const data: Record = { - metrics: metricsAggregate((ctx as any).metrics, 50), - statistics: { - activeSessions: {} - } - } - data.statistics.totalClients = sessions.sessions.size - if (admin) { - for (const wsStats of sessions.getStatistics()) { - data.statistics.activeSessions[wsStats.wsId] = wsStats - } - } - - data.health = sessions.checkHealth() - - const memU = process.memoryUsage() - data.statistics.memoryUsed = Math.round(((memU.heapUsed + memU.rss) / 1024 / 1024) * 100) / 100 - data.statistics.memoryTotal = Math.round((memU.heapTotal / 1024 / 1024) * 100) / 100 - data.statistics.memoryRSS = Math.round((memU.rss / 1024 / 1024) * 100) / 100 - data.statistics.memoryArrayBuffers = Math.round((memU.arrayBuffers / 1024 / 1024) * 100) / 100 - data.statistics.cpuUsage = Math.round(os.loadavg()[0] * 100) / 100 - data.statistics.freeMem = Math.round((os.freemem() / 1024 / 1024) * 100) / 100 - data.statistics.totalMem = Math.round((os.totalmem() / 1024 / 1024) * 100) / 100 - - return data -} - -/** - * @public - */ -export function wipeStatistics (ctx: MeasureContext): void { - const toClean: (Metrics | MetricsData)[] = [] - function cleanMetrics (m: Metrics | MetricsData): void { - m.operations = 0 - m.value = 0 - m.topResult = undefined - delete (m as Metrics).opLog - if ('measurements' in m) { - for (const v of Object.values(m.measurements)) { - toClean.push(v) - } - for (const v of Object.values(m.params)) { - for (const vv of Object.values(v)) { - toClean.push(vv) - } - } - } - } - - if (ctx instanceof MeasureMetricsContext) { - ctx.metrics.opLog = undefined - toClean.push(ctx.metrics) - while (toClean.length > 0) { - const v = toClean.shift() - if (v === undefined) { - break - } - cleanMetrics(v) - } - } -} diff --git a/server/server/src/utils.ts b/server/server/src/utils.ts deleted file mode 100644 index af482a9e502..00000000000 --- a/server/server/src/utils.ts +++ /dev/null @@ -1,91 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type WorkspaceUuid, type MeasureContext } from '@hcengineering/core' - -import type { - AddSessionActive, - AddSessionResponse, - ConnectionSocket, - Session, - SessionManager -} from '@hcengineering/server-core' - -import { type Response } from '@hcengineering/rpc' -import type { Token } from '@hcengineering/server-token' - -export interface WebsocketData { - connectionSocket?: ConnectionSocket - payload: Token - token: string - session: Promise | AddSessionResponse | undefined - url: string -} - -export function doSessionOp ( - data: WebsocketData, - op: (session: AddSessionActive, msg: Buffer) => void, - msg: Buffer -): void { - if (data.session instanceof Promise) { - // We need to copy since we will out of protected buffer area - const msgCopy = Buffer.copyBytesFrom(new Uint8Array(msg)) - void data.session - .then((_session) => { - data.session = _session - if ('session' in _session) { - op(_session, msgCopy) - } - }) - .catch((err) => { - console.error({ message: 'Failed to process session operation', err }) - }) - } else { - if (data.session !== undefined && 'session' in data.session) { - op(data.session, msg) - } - } -} - -export function processRequest ( - session: Session, - cs: ConnectionSocket, - context: MeasureContext, - workspaceId: WorkspaceUuid, - buff: any, - sessions: SessionManager -): void { - try { - const request = cs.readRequest(buff, session.binaryMode) - void sessions.handleRequest(context, session, cs, request, workspaceId).catch((err) => { - context.error('failed to handle request', { err, request }) - }) - } catch (err: any) { - if (((err.message as string) ?? '').includes('Data read, but end of buffer not reached')) { - // ignore it - } else { - throw err - } - } -} - -export function sendResponse ( - ctx: MeasureContext, - session: Session, - socket: ConnectionSocket, - resp: Response -): Promise { - return socket.send(ctx, resp, session.binaryMode, session.useCompression) -} diff --git a/server/server/src/workspace.ts b/server/server/src/workspace.ts deleted file mode 100644 index 120b49b55fb..00000000000 --- a/server/server/src/workspace.ts +++ /dev/null @@ -1,115 +0,0 @@ -// -// Copyright © 2022, 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Analytics } from '@hcengineering/analytics' -import { type Branding, type MeasureContext, type WorkspaceIds } from '@hcengineering/core' -import type { ConnectionSocket, Pipeline, Session } from '@hcengineering/server-core' - -interface TickHandler { - ticks: number - operation: () => void -} - -export type WorkspacePipelineFactory = () => Promise - -/** - * @public - */ -export class Workspace { - pipeline?: Pipeline | Promise - maintenance: boolean = false - closing?: Promise - - workspaceInitCompleted: boolean = false - - softShutdown: number - - sessions = new Map() - tickHandlers = new Map() - - operations: number = 0 - - constructor ( - readonly context: MeasureContext, - readonly token: string, // Account workspace update token. - readonly factory: WorkspacePipelineFactory, - - readonly tickHash: number, - - softShutdown: number, - - readonly wsId: WorkspaceIds, - readonly branding: Branding | null - ) { - this.softShutdown = softShutdown - } - - private getPipeline (): Pipeline | Promise { - if (this.pipeline === undefined) { - this.pipeline = this.factory() - } - return this.pipeline - } - - async with(op: (pipeline: Pipeline) => Promise): Promise { - this.operations++ - let pipeline = this.getPipeline() - if (pipeline instanceof Promise) { - pipeline = await pipeline - this.pipeline = pipeline - } - try { - return await op(pipeline) - } finally { - this.operations-- - } - } - - async close (ctx: MeasureContext): Promise { - if (this.pipeline === undefined) { - return - } - const pipeline = await this.pipeline - const closePipeline = async (): Promise => { - try { - await ctx.with('close-pipeline', {}, async () => { - await pipeline.close() - }) - } catch (err: any) { - Analytics.handleError(err) - ctx.error('close-pipeline-error', { error: err }) - } - } - - await ctx.with('closing', {}, async () => { - const to = timeoutPromise(120000) - const closePromises = closePipeline() - await Promise.race([closePromises, to.promise]) - to.cancelHandle() - }) - } -} - -function timeoutPromise (time: number): { promise: Promise, cancelHandle: () => void } { - let timer: any - return { - promise: new Promise((resolve) => { - timer = setTimeout(resolve, time) - }), - cancelHandle: () => { - clearTimeout(timer) - } - } -} diff --git a/server/server/tsconfig.json b/server/server/tsconfig.json deleted file mode 100644 index c6a877cf6c3..00000000000 --- a/server/server/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file