From e0ca033405b728646c55379eff160333441b29b6 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Wed, 13 Nov 2024 00:13:11 +0700 Subject: [PATCH] UBERF-8582: Fix triggers (#7155) Signed-off-by: Andrey Sobolev --- .vscode/launch.json | 3 +- dev/docker-compose.yaml | 36 +---- models/controlled-documents/src/migration.ts | 65 ++++----- models/recruit/src/migration.ts | 26 +++- models/server-activity/src/index.ts | 7 +- models/server-ai-bot/src/index.ts | 1 + models/server-collaboration/src/index.ts | 3 +- models/server-notification/src/index.ts | 5 +- models/server-request/src/index.ts | 3 +- models/server-tags/src/index.ts | 3 +- models/server-task/src/index.ts | 3 +- models/server-time/src/index.ts | 1 + models/server-view/src/index.ts | 10 +- packages/model/src/utils.ts | 7 +- pods/front/src/__start.ts | 6 +- pods/fulltext/src/index.ts | 7 +- pods/server/package.json | 3 +- .../activity-resources/src/index.ts | 95 +++++++------ server-plugins/ai-bot-resources/src/index.ts | 62 +++++---- server-plugins/chunter-resources/src/index.ts | 31 ++--- .../collaboration-resources/src/index.ts | 52 ++++--- .../notification-resources/src/index.ts | 51 ++++--- server-plugins/request-resources/src/index.ts | 51 ++++--- server-plugins/tags-resources/src/index.ts | 49 ++++--- server-plugins/task-resources/src/index.ts | 51 +++---- .../telegram-resources/src/index.ts | 25 ++-- server-plugins/time-resources/src/index.ts | 31 +++-- server/core/src/dbAdapterManager.ts | 17 +++ server/core/src/stats.ts | 29 ++-- server/core/src/triggers.ts | 9 +- server/core/src/types.ts | 3 + server/indexer/src/index.ts | 1 - server/indexer/src/indexer/indexer.ts | 17 ++- server/indexer/src/ydoc.ts | 49 ------- server/middleware/src/domainTx.ts | 71 +++++++--- server/middleware/src/liveQuery.ts | 4 +- server/middleware/src/queryJoin.ts | 3 +- .../middleware/src/tests/queryJoiner.spec.ts | 25 ++++ server/middleware/src/triggers.ts | 23 ++- .../github/server-github-model/src/index.ts | 1 + .../server-github-resources/src/index.ts | 131 ++++++++++-------- tests/docker-compose.yaml | 26 +++- tests/prepare.sh | 2 + tests/sanity/tests/inbox/inbox.spec.ts | 35 +++-- 44 files changed, 607 insertions(+), 526 deletions(-) delete mode 100644 server/indexer/src/ydoc.ts create mode 100644 server/middleware/src/tests/queryJoiner.spec.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index f23ca9c3fa..2c8afa3370 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -55,7 +55,8 @@ // "VERSION": "0.6.289", "ELASTIC_INDEX_NAME": "local_storage_index", "UPLOAD_URL": "/files", - "AI_BOT_URL": "http://localhost:4010" + "AI_BOT_URL": "http://localhost:4010", + "STATS_URL": "http://host.docker.internal:4900", }, "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "runtimeVersion": "20", diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 1ac0aa0855..2ad74469fb 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -102,7 +102,7 @@ services: environment: - PORT=4900 - SERVER_SECRET=secret - restart: unless-stopped + restart: unless-stopped workspace: image: hardcoreeng/workspace extra_hosts: @@ -221,7 +221,6 @@ services: links: - mongodb - minio - - rekoni - account - stats # - apm-server @@ -261,7 +260,6 @@ services: links: - postgres - minio - - rekoni - account - stats # - apm-server @@ -296,10 +294,6 @@ services: restart: unless-stopped ports: - 4004:4004 - deploy: - resources: - limits: - memory: 1024M fulltext: image: hardcoreeng/fulltext extra_hosts: @@ -319,10 +313,6 @@ services: - STATS_URL=http://host.docker.internal:4900 - REKONI_URL=http://host.docker.internal:4004 - ACCOUNTS_URL=http://host.docker.internal:3000 - deploy: - resources: - limits: - memory: 300M fulltext_pg: image: hardcoreeng/fulltext extra_hosts: @@ -343,10 +333,6 @@ services: - STATS_URL=http://host.docker.internal:4900 - REKONI_URL=http://host.docker.internal:4004 - ACCOUNTS_URL=http://host.docker.internal:3000 - deploy: - resources: - limits: - memory: 300M print: image: hardcoreeng/print extra_hosts: @@ -358,10 +344,6 @@ services: - SECRET=secret - STORAGE_CONFIG=${STORAGE_CONFIG} - STATS_URL=http://host.docker.internal:4900 - deploy: - resources: - limits: - memory: 300M sign: image: hardcoreeng/sign extra_hosts: @@ -382,10 +364,6 @@ services: - SERVICE_ID=sign-service - BRANDING_PATH=/var/cfg/branding.json - STATS_URL=http://host.docker.internal:4900 - deploy: - resources: - limits: - memory: 300M analytics: image: hardcoreeng/analytics-collector extra_hosts: @@ -402,10 +380,6 @@ services: - ACCOUNTS_URL=http://host.docker.internal:3000 - SUPPORT_WORKSPACE=support - STATS_URL=http://host.docker.internal:4900 - deploy: - resources: - limits: - memory: 300M aiBot: image: hardcoreeng/ai-bot ports: @@ -426,10 +400,6 @@ services: - STATS_URL=http://host.docker.internal:4900 # - LOVE_ENDPOINT=http://host.docker.internal:8096 # - OPENAI_API_KEY=token - deploy: - resources: - limits: - memory: 300M # telegram-bot: # image: hardcoreeng/telegram-bot # extra_hosts: @@ -445,10 +415,6 @@ services: # - ACCOUNTS_URL=http://host.docker.internal:3000 # - SERVICE_ID=telegram-bot-service # - STATS_URL=http://host.docker.internal:4900 -# deploy: -# resources: -# limits: -# memory: 300M volumes: db: dbpg: diff --git a/models/controlled-documents/src/migration.ts b/models/controlled-documents/src/migration.ts index 3c25413b42..a609ba304f 100644 --- a/models/controlled-documents/src/migration.ts +++ b/models/controlled-documents/src/migration.ts @@ -2,50 +2,51 @@ // Copyright @ 2022-2023 Hardcore Engineering Inc. // +import attachment, { type Attachment } from '@hcengineering/attachment' import { + clone, + loadCollaborativeDoc, + saveCollaborativeDoc, + YAbstractType, + YXmlElement, + YXmlText +} from '@hcengineering/collaboration' +import { + type ChangeControl, + type ControlledDocument, + createChangeControl, + createDocumentTemplate, + type DocumentCategory, + documentsId, + DocumentState +} from '@hcengineering/controlled-documents' +import { + type Class, type Data, - type Ref, - TxOperations, - generateId, + type Doc, DOMAIN_TX, + generateId, makeCollaborativeDoc, MeasureMetricsContext, - type Class, - type Doc, - SortingOrder + type Ref, + SortingOrder, + toIdMap, + TxOperations } from '@hcengineering/core' import { createDefaultSpace, createOrUpdate, - type MigrateUpdate, - type MigrationDocumentQuery, - tryMigrate, - tryUpgrade, type MigrateOperation, + type MigrateUpdate, type MigrationClient, - type MigrationUpgradeClient + type MigrationDocumentQuery, + type MigrationUpgradeClient, + tryMigrate, + tryUpgrade } from '@hcengineering/model' +import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment' import core from '@hcengineering/model-core' import tags from '@hcengineering/tags' -import { - type ChangeControl, - type DocumentCategory, - DocumentState, - documentsId, - createDocumentTemplate, - type ControlledDocument, - createChangeControl -} from '@hcengineering/controlled-documents' -import { - loadCollaborativeDoc, - saveCollaborativeDoc, - YXmlElement, - YXmlText, - YAbstractType, - clone -} from '@hcengineering/collaboration' -import attachment, { type Attachment } from '@hcengineering/attachment' -import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment' import documents, { DOMAIN_DOCUMENTS } from './index' @@ -210,6 +211,7 @@ async function createDocumentCategories (tx: TxOperations): Promise { { code: 'CM', title: 'Client Management' } ] + const catsCache = toIdMap(await tx.findAll(documents.class.DocumentCategory, {})) const ops = tx.apply() for (const c of categories) { await createOrUpdate( @@ -217,7 +219,8 @@ async function createDocumentCategories (tx: TxOperations): Promise { documents.class.DocumentCategory, documents.space.QualityDocuments, { ...c, attachments: 0 }, - ((documents.category.DOC as string) + ' - ' + c.code) as Ref + ((documents.category.DOC as string) + ' - ' + c.code) as Ref, + catsCache ) } await ops.commit() diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index 05520bd0ee..87313df3db 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -14,7 +14,15 @@ // import { getCategories } from '@anticrm/skillset' -import core, { DOMAIN_TX, TxOperations, type Ref, type Space, type Status } from '@hcengineering/core' +import core, { + DOMAIN_TX, + toIdMap, + TxOperations, + type Doc, + type Ref, + type Space, + type Status +} from '@hcengineering/core' import { createOrUpdate, migrateSpace, @@ -26,7 +34,7 @@ import { type ModelLogger } from '@hcengineering/model' import tags, { type TagCategory } from '@hcengineering/model-tags' -import task, { DOMAIN_TASK, createSequence, migrateDefaultStatusesBase } from '@hcengineering/model-task' +import task, { createSequence, DOMAIN_TASK, migrateDefaultStatusesBase } from '@hcengineering/model-task' import { recruitId, type Applicant } from '@hcengineering/recruit' import { DOMAIN_CALENDAR } from '@hcengineering/model-calendar' @@ -194,10 +202,16 @@ async function createDefaults (client: MigrationUpgradeClient, tx: TxOperations) }, recruit.category.Other ) + const ops = tx.apply() - for (const c of getCategories()) { + const cats = getCategories().filter((it, idx, arr) => arr.findIndex((qt) => qt.id === it.id) === idx) + + const existingCategories = toIdMap( + await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate }) + ) + for (const c of cats) { await createOrUpdate( - tx, + ops, tags.class.TagCategory, core.space.Workspace, { @@ -207,9 +221,11 @@ async function createDefaults (client: MigrationUpgradeClient, tx: TxOperations) tags: c.skills, default: false }, - (recruit.category.Category + '.' + c.id) as Ref + (recruit.category.Category + '.' + c.id) as Ref, + existingCategories ) } + await ops.commit() await createSequence(tx, recruit.class.Review) await createSequence(tx, recruit.class.Opinion) diff --git a/models/server-activity/src/index.ts b/models/server-activity/src/index.ts index f46cc9b2ab..72530fd081 100644 --- a/models/server-activity/src/index.ts +++ b/models/server-activity/src/index.ts @@ -44,14 +44,13 @@ export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverActivity.trigger.ActivityMessagesHandler, - txMatch: { - 'tx.objectClass': { $nin: [activity.class.ActivityMessage, notification.class.DocNotifyContext] } - }, + arrays: true, isAsync: true }) builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverActivity.trigger.OnDocRemoved + trigger: serverActivity.trigger.OnDocRemoved, + arrays: true }) builder.createDoc(serverCore.class.Trigger, core.space.Model, { diff --git a/models/server-ai-bot/src/index.ts b/models/server-ai-bot/src/index.ts index 7fd057182d..b57e134b5c 100644 --- a/models/server-ai-bot/src/index.ts +++ b/models/server-ai-bot/src/index.ts @@ -37,6 +37,7 @@ export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverAiBot.trigger.OnMessageSend, + arrays: true, isAsync: true }) diff --git a/models/server-collaboration/src/index.ts b/models/server-collaboration/src/index.ts index 30e6796737..1e7b7d8077 100644 --- a/models/server-collaboration/src/index.ts +++ b/models/server-collaboration/src/index.ts @@ -23,6 +23,7 @@ export { serverCollaborationId } from '@hcengineering/server-collaboration' export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverCollaboration.trigger.OnDelete + trigger: serverCollaboration.trigger.OnDelete, + arrays: true }) } diff --git a/models/server-notification/src/index.ts b/models/server-notification/src/index.ts index c1fcc304a1..c337b396d7 100644 --- a/models/server-notification/src/index.ts +++ b/models/server-notification/src/index.ts @@ -16,6 +16,7 @@ import { type Builder, Mixin, Model } from '@hcengineering/model' +import contact from '@hcengineering/contact' import core, { type Ref } from '@hcengineering/core' import { TClass, TDoc } from '@hcengineering/model-core' import { TNotificationType } from '@hcengineering/model-notification' @@ -33,7 +34,6 @@ import serverNotification, { type TypeMatch, type TypeMatchFunc } from '@hcengineering/server-notification' -import contact from '@hcengineering/contact' export { serverNotificationId } from '@hcengineering/server-notification' @@ -89,7 +89,8 @@ export function createModel (builder: Builder): void { }) builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverNotification.trigger.OnDocRemove + trigger: serverNotification.trigger.OnDocRemove, + arrays: true }) builder.createDoc(serverCore.class.Trigger, core.space.Model, { diff --git a/models/server-request/src/index.ts b/models/server-request/src/index.ts index 007cceb17d..50ecf97463 100644 --- a/models/server-request/src/index.ts +++ b/models/server-request/src/index.ts @@ -25,7 +25,8 @@ export { serverRequestId } from '@hcengineering/server-request' export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverRequest.trigger.OnRequest + trigger: serverRequest.trigger.OnRequest, + arrays: true }) builder.mixin(request.class.Request, core.class.Class, serverNotification.mixin.TextPresenter, { diff --git a/models/server-tags/src/index.ts b/models/server-tags/src/index.ts index 842b1d05d7..456b0a5315 100644 --- a/models/server-tags/src/index.ts +++ b/models/server-tags/src/index.ts @@ -23,7 +23,8 @@ export { serverTagsId } from '@hcengineering/server-tags' export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverTags.trigger.onTagReference + trigger: serverTags.trigger.onTagReference, + arrays: true }) builder.mixin, ObjectDDParticipant>( diff --git a/models/server-task/src/index.ts b/models/server-task/src/index.ts index 80e2227a88..c02b5d2f67 100644 --- a/models/server-task/src/index.ts +++ b/models/server-task/src/index.ts @@ -22,6 +22,7 @@ export { serverTaskId } from '@hcengineering/server-task' export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverTask.trigger.OnStateUpdate + trigger: serverTask.trigger.OnStateUpdate, + arrays: true }) } diff --git a/models/server-time/src/index.ts b/models/server-time/src/index.ts index 152eacf9f0..16266df90d 100644 --- a/models/server-time/src/index.ts +++ b/models/server-time/src/index.ts @@ -38,6 +38,7 @@ export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverTime.trigger.OnTask, + arrays: true, isAsync: true }) diff --git a/models/server-view/src/index.ts b/models/server-view/src/index.ts index dec068e8f0..ed9a64ec72 100644 --- a/models/server-view/src/index.ts +++ b/models/server-view/src/index.ts @@ -15,10 +15,10 @@ import core, { type Doc } from '@hcengineering/core' import { type Builder, Mixin } from '@hcengineering/model' -import serverCore, { type TriggerControl } from '@hcengineering/server-core' -import serverView, { type ServerLinkIdProvider } from '@hcengineering/server-view' import { TClass } from '@hcengineering/model-core' import { type Resource } from '@hcengineering/platform' +import serverCore, { type TriggerControl } from '@hcengineering/server-core' +import serverView, { type ServerLinkIdProvider } from '@hcengineering/server-view' export { serverViewId } from '@hcengineering/server-view' @@ -31,6 +31,10 @@ export function createModel (builder: Builder): void { builder.createModel(TServerLinkIdProvider) builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverView.trigger.OnCustomAttributeRemove + trigger: serverView.trigger.OnCustomAttributeRemove, + txMatch: { + _class: core.class.TxRemoveDoc, + objectClass: core.class.Attribute + } }) } diff --git a/packages/model/src/utils.ts b/packages/model/src/utils.ts index 9de31e4027..4de9fcc283 100644 --- a/packages/model/src/utils.ts +++ b/packages/model/src/utils.ts @@ -1,4 +1,4 @@ -import { Class, Data, Doc, DocumentUpdate, Ref, Space, TxOperations } from '@hcengineering/core' +import { Class, Data, Doc, DocumentUpdate, Ref, Space, TxOperations, type IdMap } from '@hcengineering/core' import { deepEqual } from 'fast-equals' function toUndef (value: any): any { @@ -40,9 +40,10 @@ export async function createOrUpdate ( _class: Ref>, space: Ref, data: Data, - _id: Ref + _id: Ref, + cache?: IdMap ): Promise { - const existingDoc = await client.findOne(_class, { _id }) + const existingDoc = cache !== undefined ? cache.get(_id) : await client.findOne(_class, { _id }) if (existingDoc !== undefined) { const { _class: _oldClass, _id, space: _oldSpace, modifiedBy, modifiedOn, ...oldData } = existingDoc if (modifiedBy === client.txFactory.account) { diff --git a/pods/front/src/__start.ts b/pods/front/src/__start.ts index e5d07fcec5..2718d0f739 100644 --- a/pods/front/src/__start.ts +++ b/pods/front/src/__start.ts @@ -3,11 +3,11 @@ // import { Analytics } from '@hcengineering/analytics' +import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service' import { MeasureMetricsContext, newMetrics } from '@hcengineering/core' import { startFront } from '@hcengineering/front/src/starter' -import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service' -import { join } from 'path' import { initStatisticsContext } from '@hcengineering/server-core' +import { join } from 'path' configureAnalytics(process.env.SENTRY_DSN, {}) Analytics.setTag('application', 'front') @@ -45,5 +45,5 @@ startFront(metricsContext, { ANALYTICS_COLLECTOR_URL: process.env.ANALYTICS_COLLECTOR_URL, AI_URL: process.env.AI_URL, TELEGRAM_BOT_URL: process.env.TELEGRAM_BOT_URL, - STATS_URL: process.env.STATS_URL + STATS_URL: process.env.STATS_API ?? process.env.STATS_URL }) diff --git a/pods/fulltext/src/index.ts b/pods/fulltext/src/index.ts index 542d5b19ce..4c6ee643d7 100644 --- a/pods/fulltext/src/index.ts +++ b/pods/fulltext/src/index.ts @@ -20,7 +20,7 @@ import { initStatisticsContext, type StorageConfiguration } from '@hcengineering import { join } from 'path' import { createElasticAdapter } from '@hcengineering/elastic' -import { createRekoniAdapter, createYDocAdapter, type FulltextDBConfiguration } from '@hcengineering/server-indexer' +import { createRekoniAdapter, type FulltextDBConfiguration } from '@hcengineering/server-indexer' import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage' import { readFileSync } from 'fs' import { startIndexer } from './server' @@ -80,11 +80,6 @@ const config: FulltextDBConfiguration = { factory: createRekoniAdapter, contentType: '*', url: rekoniUrl - }, - YDoc: { - factory: createYDocAdapter, - contentType: 'application/ydoc', - url: '' } }, defaultContentAdapter: 'Rekoni' diff --git a/pods/server/package.json b/pods/server/package.json index 8e4c2cb14d..84e5f4e05a 100644 --- a/pods/server/package.json +++ b/pods/server/package.json @@ -8,8 +8,7 @@ "template": "@hcengineering/node-package", "license": "EPL-2.0", "scripts": { - "start": "rush bundle --to @hcengineering/pod-server && cross-env NODE_ENV=production MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret OPERATION_PROFILING=false MODEL_JSON=../../models/all/bundle/model.json node --inspect bundle/bundle.js", - "start-u": "rush bundle --to @hcengineering/pod-server && ./bundle/ && cross-env NODE_ENV=production MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret MODEL_JSON=../../models/all/bundle/model.json node bundle/bundle.js", + "start": "rush bundle --to @hcengineering/pod-server && cross-env NODE_ENV=production MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret OPERATION_PROFILING=false MODEL_JSON=../../models/all/bundle/model.json STATS_URL=http://host.docker.internal:4900 node --inspect bundle/bundle.js", "start-flame": "rush bundle --to @hcengineering/pod-server && cross-env NODE_ENV=production MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret MODEL_JSON=../../models/all/bundle/model.json clinic flame --dest ./out -- node --nolazy -r ts-node/register --enable-source-maps src/__start.ts", "build": "compile", "_phase:bundle": "rushx bundle", diff --git a/server-plugins/activity-resources/src/index.ts b/server-plugins/activity-resources/src/index.ts index c690f91552..5811c291f3 100644 --- a/server-plugins/activity-resources/src/index.ts +++ b/server-plugins/activity-resources/src/index.ts @@ -387,69 +387,72 @@ export async function generateDocUpdateMessages ( return res } -async function ActivityMessagesHandler (tx: TxCUD, control: TriggerControl): Promise { - if ( - control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage) || - control.hierarchy.isDerived(tx.objectClass, notification.class.DocNotifyContext) || - control.hierarchy.isDerived(tx.objectClass, notification.class.ActivityInboxNotification) || - control.hierarchy.isDerived(tx.objectClass, notification.class.BrowserNotification) - ) { - return [] - } +async function ActivityMessagesHandler (_txes: TxCUD[], control: TriggerControl): Promise { + const ltxes = _txes.filter( + (it) => + !( + control.hierarchy.isDerived(it.objectClass, activity.class.ActivityMessage) || + control.hierarchy.isDerived(it.objectClass, notification.class.DocNotifyContext) || + control.hierarchy.isDerived(it.objectClass, notification.class.ActivityInboxNotification) || + control.hierarchy.isDerived(it.objectClass, notification.class.BrowserNotification) + ) + ) const cache: DocObjectCache = control.contextCache.get('ActivityMessagesHandler') ?? { docs: new Map(), transactions: new Map() } control.contextCache.set('ActivityMessagesHandler', cache) + const result: Tx[] = [] + for (const tx of ltxes) { + const txes = + tx.space === core.space.DerivedTx + ? [] + : await control.ctx.with('generateDocUpdateMessages', {}, (ctx) => + generateDocUpdateMessages(ctx, tx, control, [], undefined, cache) + ) - const txes = - tx.space === core.space.DerivedTx - ? [] - : await control.ctx.with( - 'generateDocUpdateMessages', - {}, - async (ctx) => await generateDocUpdateMessages(ctx, tx, control, [], undefined, cache) - ) + const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc)) - const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc)) - - const notificationTxes = await control.ctx.with( - 'createCollaboratorNotifications', - {}, - async (ctx) => - await createCollaboratorNotifications(ctx, tx, control, messages, undefined, cache.docs as Map, Doc>) - ) - - const result = [...txes, ...notificationTxes] + const notificationTxes = await control.ctx.with('createCollaboratorNotifications', {}, (ctx) => + createCollaboratorNotifications(ctx, tx, control, messages, undefined, cache.docs as Map, Doc>) + ) + result.push(...txes, ...notificationTxes) + } if (result.length > 0) { await control.apply(control.ctx, result) } return [] } -async function OnDocRemoved (originTx: TxCUD, control: TriggerControl): Promise { - const tx = TxProcessor.extractTx(originTx) as TxCUD +async function OnDocRemoved (txes: TxCUD[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const originTx of txes) { + const tx = TxProcessor.extractTx(originTx) as TxCUD - if (tx._class !== core.class.TxRemoveDoc) { - return [] + if (tx._class !== core.class.TxRemoveDoc) { + continue + } + + const activityDocMixin = control.hierarchy.classHierarchyMixin(tx.objectClass, activity.mixin.ActivityDoc) + + if (activityDocMixin === undefined) { + continue + } + + const messages = await control.findAll( + control.ctx, + activity.class.ActivityMessage, + { attachedTo: tx.objectId }, + { projection: { _id: 1, _class: 1, space: 1 } } + ) + + result.push( + ...messages.map((message) => control.txFactory.createTxRemoveDoc(message._class, message.space, message._id)) + ) } - - const activityDocMixin = control.hierarchy.classHierarchyMixin(tx.objectClass, activity.mixin.ActivityDoc) - - if (activityDocMixin === undefined) { - return [] - } - - const messages = await control.findAll( - control.ctx, - activity.class.ActivityMessage, - { attachedTo: tx.objectId }, - { projection: { _id: 1, _class: 1, space: 1 } } - ) - - return messages.map((message) => control.txFactory.createTxRemoveDoc(message._class, message.space, message._id)) + return result } async function ReactionNotificationContentProvider ( diff --git a/server-plugins/ai-bot-resources/src/index.ts b/server-plugins/ai-bot-resources/src/index.ts index c6910e4280..6f558240d5 100644 --- a/server-plugins/ai-bot-resources/src/index.ts +++ b/server-plugins/ai-bot-resources/src/index.ts @@ -13,6 +13,15 @@ // limitations under the License. // +import aiBot, { + aiBotAccountEmail, + AIEventType, + AIMessageEventRequest, + AITransferEventRequest +} from '@hcengineering/ai-bot' +import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector' +import chunter, { ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter' +import contact, { PersonAccount } from '@hcengineering/contact' import core, { AccountRole, AttachedDoc, @@ -27,17 +36,8 @@ import core, { TxUpdateDoc, UserStatus } from '@hcengineering/core' -import { TriggerControl } from '@hcengineering/server-core' -import chunter, { ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter' -import aiBot, { - aiBotAccountEmail, - AIEventType, - AIMessageEventRequest, - AITransferEventRequest -} from '@hcengineering/ai-bot' -import contact, { PersonAccount } from '@hcengineering/contact' import { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification' -import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector' +import { TriggerControl } from '@hcengineering/server-core' import { createAccountRequest, getSupportWorkspaceId, sendAIEvents } from './utils' @@ -241,34 +241,38 @@ async function onSupportWorkspaceMessage (control: TriggerControl, message: Chat } export async function OnMessageSend ( - originTx: TxCollectionCUD, + originTxs: TxCollectionCUD[], control: TriggerControl ): Promise { const { hierarchy } = control - const tx = TxProcessor.extractTx(originTx) as TxCreateDoc - if (tx._class !== core.class.TxCreateDoc || !hierarchy.isDerived(tx.objectClass, chunter.class.ChatMessage)) { + const txes = originTxs + .map((it) => TxProcessor.extractTx(it) as TxCreateDoc) + .filter( + (it) => + it._class === core.class.TxCreateDoc && + hierarchy.isDerived(it.objectClass, chunter.class.ChatMessage) && + !(it.modifiedBy === aiBot.account.AIBot || it.modifiedBy === core.account.System) + ) + if (txes.length === 0) { return [] } + for (const tx of txes) { + const isThread = hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage) + const message = TxProcessor.createDoc2Doc(tx) - if (tx.modifiedBy === aiBot.account.AIBot || tx.modifiedBy === core.account.System) { - return [] - } + const docClass = isThread ? (message as ThreadMessage).objectClass : message.attachedToClass - const isThread = hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage) - const message = TxProcessor.createDoc2Doc(tx) + if (!hierarchy.isDerived(docClass, chunter.class.ChunterSpace)) { + continue + } - const docClass = isThread ? (message as ThreadMessage).objectClass : message.attachedToClass + if (docClass === chunter.class.DirectMessage) { + await onBotDirectMessageSend(control, message) + } - if (!hierarchy.isDerived(docClass, chunter.class.ChunterSpace)) { - return [] - } - - if (docClass === chunter.class.DirectMessage) { - await onBotDirectMessageSend(control, message) - } - - if (docClass === analyticsCollector.class.OnboardingChannel) { - await onSupportWorkspaceMessage(control, message) + if (docClass === analyticsCollector.class.OnboardingChannel) { + await onSupportWorkspaceMessage(control, message) + } } return [] diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index c8bbeadd10..76e99455b6 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -35,7 +35,8 @@ import core, { TxProcessor, TxRemoveDoc, TxUpdateDoc, - UserStatus + UserStatus, + type MeasureContext } from '@hcengineering/core' import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification' import { getMetadata, IntlString, translate } from '@hcengineering/platform' @@ -151,7 +152,7 @@ async function OnThreadMessageCreated (originTx: TxCUD, control: TriggerCon return [lastReplyTx, repliedPersonTx] } -async function OnChatMessageCreated (tx: TxCUD, control: TriggerControl): Promise { +async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD, control: TriggerControl): Promise { const hierarchy = control.hierarchy const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc @@ -163,9 +164,7 @@ async function OnChatMessageCreated (tx: TxCUD, control: TriggerControl): P return [] } - const targetDoc = ( - await control.findAll(control.ctx, message.attachedToClass, { _id: message.attachedTo }, { limit: 1 }) - )[0] + const targetDoc = (await control.findAll(ctx, message.attachedToClass, { _id: message.attachedTo }, { limit: 1 }))[0] if (targetDoc === undefined) { return [] } @@ -190,7 +189,7 @@ async function OnChatMessageCreated (tx: TxCUD, control: TriggerControl): P ) } } else { - const collaborators = await getDocCollaborators(control.ctx, targetDoc, mixin, control) + const collaborators = await getDocCollaborators(ctx, targetDoc, mixin, control) if (!collaborators.includes(message.modifiedBy)) { collaborators.push(message.modifiedBy) } @@ -272,33 +271,19 @@ export async function ChunterTrigger (tx: TxCUD, control: TriggerControl): actualTx._class === core.class.TxCreateDoc && control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage) ) { - res.push( - ...(await control.ctx.with( - 'OnThreadMessageCreated', - {}, - async (ctx) => await OnThreadMessageCreated(tx, control) - )) - ) + res.push(...(await control.ctx.with('OnThreadMessageCreated', {}, (ctx) => OnThreadMessageCreated(tx, control)))) } if ( actualTx._class === core.class.TxRemoveDoc && control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage) ) { - res.push( - ...(await control.ctx.with( - 'OnThreadMessageDeleted', - {}, - async (ctx) => await OnThreadMessageDeleted(tx, control) - )) - ) + res.push(...(await control.ctx.with('OnThreadMessageDeleted', {}, (ctx) => OnThreadMessageDeleted(tx, control)))) } if ( actualTx._class === core.class.TxCreateDoc && control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ChatMessage) ) { - res.push( - ...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control))) - ) + res.push(...(await control.ctx.with('OnChatMessageCreated', {}, (ctx) => OnChatMessageCreated(ctx, tx, control)))) } return res } diff --git a/server-plugins/collaboration-resources/src/index.ts b/server-plugins/collaboration-resources/src/index.ts index 1a6673e8a9..4e03ebf162 100644 --- a/server-plugins/collaboration-resources/src/index.ts +++ b/server-plugins/collaboration-resources/src/index.ts @@ -13,48 +13,46 @@ // limitations under the License. // +import { removeCollaborativeDoc } from '@hcengineering/collaboration' import type { CollaborativeDoc, Doc, Tx, TxRemoveDoc } from '@hcengineering/core' import core, { TxProcessor } from '@hcengineering/core' -import { removeCollaborativeDoc } from '@hcengineering/collaboration' import { type TriggerControl } from '@hcengineering/server-core' /** * @public */ export async function OnDelete ( - tx: Tx, + txes: Tx[], { hierarchy, storageAdapter, workspace, removedMap, ctx }: TriggerControl ): Promise { - const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc + const ltxes = txes + .map((it) => TxProcessor.extractTx(it)) + .filter((it) => it._class === core.class.TxRemoveDoc) as TxRemoveDoc[] + for (const rmTx of ltxes) { + // Obtain document being deleted + const doc = removedMap.get(rmTx.objectId) + if (doc === undefined) { + continue + } - if (rmTx._class !== core.class.TxRemoveDoc) { - return [] - } + // Ids of files to delete from storage + const toDelete: CollaborativeDoc[] = [] - // Obtain document being deleted - const doc = removedMap.get(rmTx.objectId) - if (doc === undefined) { - return [] - } - - // Ids of files to delete from storage - const toDelete: CollaborativeDoc[] = [] - - const attributes = hierarchy.getAllAttributes(rmTx.objectClass) - for (const attribute of attributes.values()) { - if (hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeDoc)) { - const value = (doc as any)[attribute.name] as CollaborativeDoc - if (value !== undefined) { - toDelete.push(value) + const attributes = hierarchy.getAllAttributes(rmTx.objectClass) + for (const attribute of attributes.values()) { + if (hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeDoc)) { + const value = (doc as any)[attribute.name] as CollaborativeDoc + if (value !== undefined) { + toDelete.push(value) + } } } + + // TODO This is not accurate way to delete collaborative document + // Even though we are deleting it here, the document can be currently in use by someone else + // and when editing session ends, the collborator service will recreate the document again + await removeCollaborativeDoc(storageAdapter, workspace, toDelete, ctx) } - - // TODO This is not accurate way to delete collaborative document - // Even though we are deleting it here, the document can be currently in use by someone else - // and when editing session ends, the collborator service will recreate the document again - await removeCollaborativeDoc(storageAdapter, workspace, toDelete, ctx) - return [] } diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index 20b78adcbd..8910b19f37 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -1001,9 +1001,10 @@ export async function createCollabDocInfo ( } const settings = await getNotificationProviderControl(ctx, control) - const subscriptions = await control.findAll(ctx, notification.class.PushSubscription, { - user: { $in: Array.from(targets) } - }) + const subscriptions = (await control.queryFind(ctx, notification.class.PushSubscription, {})).filter((it) => + targets.has(it.user as Ref) + ) + for (const target of targets) { const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(target)) @@ -1938,36 +1939,34 @@ async function OnEmployeeDeactivate (tx: TxCUD, control: TriggerControl): P return res } -async function OnDocRemove (originTx: TxCUD, control: TriggerControl): Promise { - const tx = TxProcessor.extractTx(originTx) as TxRemoveDoc - - if (tx._class !== core.class.TxRemoveDoc) return [] - +async function OnDocRemove (txes: TxCUD[], control: TriggerControl): Promise { + const ltxes = txes + .map((it) => TxProcessor.extractTx(it)) + .filter((it) => it._class === core.class.TxRemoveDoc) as TxRemoveDoc[] const res: Tx[] = [] + for (const tx of ltxes) { + if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) { + const message = control.removedMap.get(tx.objectId) as ActivityMessage | undefined - if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) { - const message = control.removedMap.get(tx.objectId) as ActivityMessage | undefined - - if (message !== undefined) { - const txes = await OnActivityMessageRemove(message, control) - res.push(...txes) - } - } else if (control.hierarchy.isDerived(tx.objectClass, notification.class.DocNotifyContext)) { - const contextsCache: ContextsCache | undefined = control.cache.get(ContextsCacheKey) - if (contextsCache !== undefined) { - for (const [key, value] of contextsCache.contexts.entries()) { - if (value === tx.objectId) { - contextsCache.contexts.delete(key) + if (message !== undefined) { + const txes = await OnActivityMessageRemove(message, control) + res.push(...txes) + } + } else if (control.hierarchy.isDerived(tx.objectClass, notification.class.DocNotifyContext)) { + const contextsCache: ContextsCache | undefined = control.cache.get(ContextsCacheKey) + if (contextsCache !== undefined) { + for (const [key, value] of contextsCache.contexts.entries()) { + if (value === tx.objectId) { + contextsCache.contexts.delete(key) + } } } + + res.push(...(await removeContextNotifications(control, [tx.objectId as Ref]))) } - return await removeContextNotifications(control, [tx.objectId as Ref]) + res.push(...(await removeCollaboratorDoc(tx, control))) } - - const txes = await removeCollaboratorDoc(tx, control) - - res.push(...txes) return res } diff --git a/server-plugins/request-resources/src/index.ts b/server-plugins/request-resources/src/index.ts index 26dfdf9c34..ac49558514 100644 --- a/server-plugins/request-resources/src/index.ts +++ b/server-plugins/request-resources/src/index.ts @@ -13,54 +13,50 @@ // limitations under the License. // +import { DocUpdateMessage } from '@hcengineering/activity' +import { PersonAccount } from '@hcengineering/contact' import core, { Doc, + Ref, Tx, TxCUD, TxCollectionCUD, TxCreateDoc, - TxUpdateDoc, TxProcessor, - Ref, + TxUpdateDoc, type MeasureContext } from '@hcengineering/core' -import request, { Request, RequestStatus } from '@hcengineering/request' -import { getResource, translate } from '@hcengineering/platform' -import type { TriggerControl } from '@hcengineering/server-core' -import { pushDocUpdateMessages } from '@hcengineering/server-activity-resources' -import { DocUpdateMessage } from '@hcengineering/activity' import notification from '@hcengineering/notification' +import { getResource, translate } from '@hcengineering/platform' +import request, { Request, RequestStatus } from '@hcengineering/request' +import { pushDocUpdateMessages } from '@hcengineering/server-activity-resources' +import type { TriggerControl } from '@hcengineering/server-core' import { - getNotificationTxes, getCollaborators, + getNotificationProviderControl, + getNotificationTxes, getTextPresenter, getUsersInfo, - toReceiverInfo, - getNotificationProviderControl + toReceiverInfo } from '@hcengineering/server-notification-resources' -import { PersonAccount } from '@hcengineering/contact' /** * @public */ -export async function OnRequest (tx: Tx, control: TriggerControl): Promise { - if (tx._class !== core.class.TxCollectionCUD) { - return [] - } - +export async function OnRequest (txes: Tx[], control: TriggerControl): Promise { const hierarchy = control.hierarchy - const ptx = tx as TxCollectionCUD - - if (!hierarchy.isDerived(ptx.tx.objectClass, request.class.Request)) { - return [] - } + const ltxes = ( + txes.filter((it) => it._class === core.class.TxCollectionCUD) as TxCollectionCUD[] + ).filter((it) => hierarchy.isDerived(it.tx.objectClass, request.class.Request)) let res: Tx[] = [] - res = res.concat(await getRequestNotificationTx(control.ctx, ptx, control)) + for (const ptx of ltxes) { + res = res.concat(await getRequestNotificationTx(control.ctx, ptx, control)) - if (ptx.tx._class === core.class.TxUpdateDoc) { - res = res.concat(await OnRequestUpdate(ptx, control)) + if (ptx.tx._class === core.class.TxUpdateDoc) { + res = res.concat(await OnRequestUpdate(ptx, control)) + } } return res @@ -168,9 +164,10 @@ async function getRequestNotificationTx ( } const notificationControl = await getNotificationProviderControl(ctx, control) - const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { - user: { $in: collaborators } - }) + const collaboratorsSet = new Set(collaborators) + const subscriptions = (await control.queryFind(control.ctx, notification.class.PushSubscription, {})).filter((it) => + collaboratorsSet.has(it.user) + ) for (const target of collaborators) { const targetInfo = toReceiverInfo(control.hierarchy, usersInfo.get(target)) if (targetInfo === undefined) continue diff --git a/server-plugins/tags-resources/src/index.ts b/server-plugins/tags-resources/src/index.ts index 7b2e48ce1b..9fb608c109 100644 --- a/server-plugins/tags-resources/src/index.ts +++ b/server-plugins/tags-resources/src/index.ts @@ -49,32 +49,37 @@ export async function TagElementRemove ( /** * @public */ -export async function onTagReference (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) - const isCreate = control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc) - const isRemove = control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc) - if (!isCreate && !isRemove) return [] - if (!control.hierarchy.isDerived((actualTx as TxCUD).objectClass, tags.class.TagReference)) return [] - if (isCreate) { - const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) - const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, { - $inc: { refCount: 1 } - }) - return [res] - } - if (isRemove) { - const ctx = actualTx as TxRemoveDoc - const doc = control.removedMap.get(ctx.objectId) as TagReference - if (doc !== undefined) { - if (!control.removedMap.has(doc.tag)) { - const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, { - $inc: { refCount: -1 } +export async function onTagReference (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) + const isCreate = control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc) + const isRemove = control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc) + if (!isCreate && !isRemove) return [] + if (!control.hierarchy.isDerived((actualTx as TxCUD).objectClass, tags.class.TagReference)) return [] + if (isCreate) { + const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) + result.push( + control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, { + $inc: { refCount: 1 } }) - return [res] + ) + } + if (isRemove) { + const ctx = actualTx as TxRemoveDoc + const doc = control.removedMap.get(ctx.objectId) as TagReference + if (doc !== undefined) { + if (!control.removedMap.has(doc.tag)) { + result.push( + control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, { + $inc: { refCount: -1 } + }) + ) + } } } } - return [] + return result } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type diff --git a/server-plugins/task-resources/src/index.ts b/server-plugins/task-resources/src/index.ts index 4942d5da45..d39ec634a3 100644 --- a/server-plugins/task-resources/src/index.ts +++ b/server-plugins/task-resources/src/index.ts @@ -20,31 +20,34 @@ import task, { Task } from '@hcengineering/task' /** * @public */ -export async function OnStateUpdate (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCUD - if (!control.hierarchy.isDerived(actualTx.objectClass, task.class.Task)) return [] - if (actualTx._class === core.class.TxCreateDoc) { - const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) - const status = (await control.modelDb.findAll(core.class.Status, { _id: doc.status }))[0] - if (status?.category === task.statusCategory.Lost || status?.category === task.statusCategory.Won) { - return [control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { isDone: true })] - } - } else if (actualTx._class === core.class.TxUpdateDoc) { - const updateTx = actualTx as TxUpdateDoc - if (updateTx.operations.status !== undefined) { - const status = (await control.modelDb.findAll(core.class.Status, { _id: updateTx.operations.status }))[0] +export async function OnStateUpdate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCUD + if (!control.hierarchy.isDerived(actualTx.objectClass, task.class.Task)) return [] + if (actualTx._class === core.class.TxCreateDoc) { + const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) + const status = control.modelDb.findAllSync(core.class.Status, { _id: doc.status })[0] if (status?.category === task.statusCategory.Lost || status?.category === task.statusCategory.Won) { - return [ - control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, { - isDone: true - }) - ] - } else { - return [ - control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, { - isDone: false - }) - ] + result.push(control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { isDone: true })) + } + } else if (actualTx._class === core.class.TxUpdateDoc) { + const updateTx = actualTx as TxUpdateDoc + if (updateTx.operations.status !== undefined) { + const status = control.modelDb.findAllSync(core.class.Status, { _id: updateTx.operations.status })[0] + if (status?.category === task.statusCategory.Lost || status?.category === task.statusCategory.Won) { + result.push( + control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, { + isDone: true + }) + ) + } else { + result.push( + control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, { + isDone: false + }) + ) + } } } } diff --git a/server-plugins/telegram-resources/src/index.ts b/server-plugins/telegram-resources/src/index.ts index 7e9af9c0db..bc62bd1101 100644 --- a/server-plugins/telegram-resources/src/index.ts +++ b/server-plugins/telegram-resources/src/index.ts @@ -13,6 +13,8 @@ // limitations under the License. // +import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' +import chunter, { ChatMessage } from '@hcengineering/chunter' import contact, { Channel, ChannelProvider, Contact, Employee, formatName, PersonAccount } from '@hcengineering/contact' import { Account, @@ -29,27 +31,25 @@ import { TxCreateDoc, TxProcessor } from '@hcengineering/core' -import { TriggerControl } from '@hcengineering/server-core' import notification, { BaseNotificationType, InboxNotification, MentionInboxNotification, NotificationType } from '@hcengineering/notification' -import telegram, { TelegramMessage, TelegramNotificationRequest } from '@hcengineering/telegram' -import setting, { Integration } from '@hcengineering/setting' -import { NotificationProviderFunc, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification' import { getMetadata, getResource, translate } from '@hcengineering/platform' -import serverTelegram from '@hcengineering/server-telegram' +import { TriggerControl } from '@hcengineering/server-core' +import { NotificationProviderFunc, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification' import { - getTranslatedNotificationContent, + getNotificationLink, getTextPresenter, - getNotificationLink + getTranslatedNotificationContent } from '@hcengineering/server-notification-resources' +import serverTelegram from '@hcengineering/server-telegram' import { generateToken } from '@hcengineering/server-token' -import chunter, { ChatMessage } from '@hcengineering/chunter' +import setting, { Integration } from '@hcengineering/setting' +import telegram, { TelegramMessage, TelegramNotificationRequest } from '@hcengineering/telegram' import { markupToHTML } from '@hcengineering/text' -import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' /** * @public @@ -245,6 +245,7 @@ function hasAttachments (doc: ActivityMessage | undefined, hierarchy: Hierarchy) return false } +const telegramNotificationKey = 'telegram.notification.reported' const SendTelegramNotifications: NotificationProviderFunc = async ( control: TriggerControl, types: BaseNotificationType[], @@ -261,7 +262,11 @@ const SendTelegramNotifications: NotificationProviderFunc = async ( const botUrl = getMetadata(serverTelegram.metadata.BotUrl) if (botUrl === undefined || botUrl === '') { - control.ctx.error('Please provide telegram bot service url to enable telegram notifications.') + const reported = control.cache.get(telegramNotificationKey) + if (reported === undefined) { + control.ctx.error('Please provide telegram bot service url to enable telegram notifications.') + control.cache.set(telegramNotificationKey, true) + } return [] } diff --git a/server-plugins/time-resources/src/index.ts b/server-plugins/time-resources/src/index.ts index 49941a34e7..324011110c 100644 --- a/server-plugins/time-resources/src/index.ts +++ b/server-plugins/time-resources/src/index.ts @@ -50,23 +50,26 @@ import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengine /** * @public */ -export async function OnTask (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCUD - const mixin = control.hierarchy.classHierarchyMixin, ToDoFactory>( - actualTx.objectClass, - serverTime.mixin.ToDoFactory - ) - if (mixin !== undefined) { - if (actualTx._class !== core.class.TxRemoveDoc) { - const factory = await getResource(mixin.factory) - return await factory(tx, control) - } else { - const todos = await control.findAll(control.ctx, time.class.ToDo, { attachedTo: actualTx.objectId }) - return todos.map((p) => control.txFactory.createTxRemoveDoc(p._class, p.space, p._id)) +export async function OnTask (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCUD + const mixin = control.hierarchy.classHierarchyMixin, ToDoFactory>( + actualTx.objectClass, + serverTime.mixin.ToDoFactory + ) + if (mixin !== undefined) { + if (actualTx._class !== core.class.TxRemoveDoc) { + const factory = await getResource(mixin.factory) + result.push(...(await factory(tx, control))) + } else { + const todos = await control.findAll(control.ctx, time.class.ToDo, { attachedTo: actualTx.objectId }) + result.push(...todos.map((p) => control.txFactory.createTxRemoveDoc(p._class, p.space, p._id))) + } } } - return [] + return result } export async function OnWorkSlotUpdate (tx: Tx, control: TriggerControl): Promise { diff --git a/server/core/src/dbAdapterManager.ts b/server/core/src/dbAdapterManager.ts index e03fff9417..1243b184ca 100644 --- a/server/core/src/dbAdapterManager.ts +++ b/server/core/src/dbAdapterManager.ts @@ -169,6 +169,23 @@ export class DbAdapterManagerImpl implements DBAdapterManager { } } + 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 diff --git a/server/core/src/stats.ts b/server/core/src/stats.ts index eab940a388..3ede8483e4 100644 --- a/server/core/src/stats.ts +++ b/server/core/src/stats.ts @@ -126,21 +126,28 @@ export function initStatisticsContext ( workspaces: ops?.getUsers?.() } - void fetch( - concatLink(statsUrl, '/api/v1/statistics') + `/?token=${encodeURIComponent(token)}&name=${serviceId}`, - { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - } - ).catch((err) => { + try { + void fetch( + concatLink(statsUrl, '/api/v1/statistics') + `/?token=${encodeURIComponent(token)}&name=${serviceId}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + } + ).catch((err) => { + errorToSend++ + if (errorToSend % 20 === 0) { + console.error(err) + } + }) + } catch (err: any) { errorToSend++ if (errorToSend % 20 === 0) { console.error(err) } - }) + } } }, METRICS_UPDATE_INTERVAL) diff --git a/server/core/src/triggers.ts b/server/core/src/triggers.ts index 7ed316b42b..0266a927c8 100644 --- a/server/core/src/triggers.ts +++ b/server/core/src/triggers.ts @@ -17,7 +17,6 @@ import core, { TxFactory, TxProcessor, - generateId, groupByArray, matchQuery, type Class, @@ -134,7 +133,6 @@ export class Triggers { mode: 'sync' | 'async' ): Promise { const result: Tx[] = [] - for (const { query, trigger, arrays } of this.triggers) { if ((trigger.isAsync ? 'async' : 'sync') !== mode) { continue @@ -150,19 +148,14 @@ export class Triggers { trigger.resource, {}, async (ctx) => { - if (mode === 'async') { - ctx.id = generateId() - } const tresult = await this.applyTrigger(ctx, ctrl, matches, { trigger, arrays }) result.push(...tresult) - if (ctx.onEnd !== undefined && mode === 'async') { - await ctx.onEnd(ctx) - } }, { count: matches.length, arrays } ) } } + return result } diff --git a/server/core/src/types.ts b/server/core/src/types.ts index 21deefcec7..f3697430b1 100644 --- a/server/core/src/types.ts +++ b/server/core/src/types.ts @@ -141,6 +141,9 @@ 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 diff --git a/server/indexer/src/index.ts b/server/indexer/src/index.ts index f1ae8ddc0d..5567dcd691 100644 --- a/server/indexer/src/index.ts +++ b/server/indexer/src/index.ts @@ -18,7 +18,6 @@ import type { ContentTextAdapterConfiguration, FullTextAdapterFactory } from '@h export * from './fulltext' export * from './indexer' export * from './rekoni' -export * from './ydoc' export interface FulltextDBConfiguration { fulltextAdapter: { diff --git a/server/indexer/src/indexer/indexer.ts b/server/indexer/src/indexer/indexer.ts index 34ecb2c100..3d74b393ce 100644 --- a/server/indexer/src/indexer/indexer.ts +++ b/server/indexer/src/indexer/indexer.ts @@ -63,7 +63,7 @@ import type { StorageAdapter } from '@hcengineering/server-core' import { RateLimiter, SessionDataImpl } from '@hcengineering/server-core' -import { jsonToText, markupToJSON } from '@hcengineering/text' +import { jsonToText, markupToJSON, pmNodeToText, yDocContentToNodes } from '@hcengineering/text' import { findSearchPresenter, updateDocWithPresenter } from '../mapper' import { type FullTextPipeline } from './types' import { createIndexedDoc, createStateDoc, getContent } from './utils' @@ -786,12 +786,21 @@ export class FullTextIndexPipeline implements FullTextPipeline { if (collaborativeDoc !== undefined && collaborativeDoc !== '') { const { documentId } = collaborativeDocParse(collaborativeDoc) - const docInfo: Blob | undefined = await this.storageAdapter?.stat(ctx, this.workspace, documentId) try { - await this.handleBlob(ctx, docInfo, indexedDoc) + const readable = await this.storageAdapter?.read(ctx, this.workspace, documentId) + const nodes = yDocContentToNodes(Buffer.concat(readable as any)) + let textContent = nodes.map(pmNodeToText).join('\n') + textContent = textContent + .split(/ +|\t+|\f+/) + .filter((it) => it) + .join(' ') + .split(/\n\n+/) + .join('\n') + + indexedDoc.fulltextSummary += '\n' + textContent } catch (err: any) { Analytics.handleError(err) - ctx.error('failed to handle blob', { _id: docInfo?._id, workspace: this.workspace.name }) + ctx.error('failed to handle blob', { _id: documentId, workspace: this.workspace.name }) } } } diff --git a/server/indexer/src/ydoc.ts b/server/indexer/src/ydoc.ts deleted file mode 100644 index 140a2bd519..0000000000 --- a/server/indexer/src/ydoc.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { type MeasureContext, type WorkspaceId } from '@hcengineering/core' -import { type ContentTextAdapter } from '@hcengineering/server-core' -import { pmNodeToText, yDocContentToNodes } from '@hcengineering/text' -import { Buffer } from 'node:buffer' -import { Readable } from 'node:stream' - -/** - * @public - */ -export async function createYDocAdapter (_url: string): Promise { - return { - content: async ( - ctx: MeasureContext, - workspace: WorkspaceId, - _name: string, - _type: string, - data: Readable | Buffer | string - ): Promise => { - const chunks: any[] = [] - - if (data instanceof Readable) { - await new Promise((resolve) => { - data.on('readable', () => { - let chunk: any - while ((chunk = data.read()) !== null) { - const b = chunk as Buffer - chunks.push(b) - } - }) - - data.on('end', () => { - resolve(null) - }) - }) - } else if (data instanceof Buffer) { - chunks.push(data) - } else { - console.warn('ydoc content adapter does not support string content') - } - - if (chunks.length > 0) { - const nodes = yDocContentToNodes(Buffer.concat(chunks)) - return nodes.map(pmNodeToText).join('\n') - } - - return '' - } - } -} diff --git a/server/middleware/src/domainTx.ts b/server/middleware/src/domainTx.ts index 654c50faae..1bde9158c1 100644 --- a/server/middleware/src/domainTx.ts +++ b/server/middleware/src/domainTx.ts @@ -14,9 +14,10 @@ // import core, { + Domain, + groupByArray, TxProcessor, type Doc, - type Domain, type MeasureContext, type SessionData, type Tx, @@ -24,7 +25,13 @@ import core, { type TxResult } from '@hcengineering/core' import { PlatformError, unknownError } from '@hcengineering/platform' -import type { DBAdapterManager, Middleware, PipelineContext, TxMiddlewareResult } from '@hcengineering/server-core' +import type { + DbAdapter, + DBAdapterManager, + Middleware, + PipelineContext, + TxMiddlewareResult +} from '@hcengineering/server-core' import { BaseMiddleware } from '@hcengineering/server-core' /** @@ -66,32 +73,41 @@ export class DomainTxMiddleware extends BaseMiddleware implements Middleware { private async routeTx (ctx: MeasureContext, txes: Tx[]): Promise { const result: TxResult[] = [] - const domainGroups = new Map[]>() + const adapterGroups = new Map[]>() - const routeToAdapter = async (domain: Domain, txes: TxCUD[]): Promise => { + const routeToAdapter = async (adapter: DbAdapter, txes: TxCUD[]): Promise => { if (txes.length > 0) { // Find all deleted documents - - const adapter = this.adapterManager.getAdapter(domain, true) - const toDelete = txes.filter((it) => it._class === core.class.TxRemoveDoc).map((it) => it.objectId) + const toDelete = txes.filter((it) => it._class === core.class.TxRemoveDoc) if (toDelete.length > 0) { - const toDeleteDocs = await ctx.with( - 'adapter-load', - { domain }, - async () => await adapter.load(ctx, domain, toDelete), - { count: toDelete.length } - ) + const deleteByDomain = groupByArray(toDelete, (it) => this.context.hierarchy.getDomain(it.objectClass)) - for (const ddoc of toDeleteDocs) { - ctx.contextData.removedMap.set(ddoc._id, ddoc) + for (const [d, docs] of deleteByDomain.entries()) { + const todel = await ctx.with( + 'adapter-load', + {}, + () => + adapter.load( + ctx, + d, + docs.map((it) => it._id) + ), + { count: toDelete.length } + ) + + for (const ddoc of todel) { + ctx.contextData.removedMap.set(ddoc._id, ddoc) + } } } - const r = await ctx.with('adapter-tx', { domain }, (ctx) => adapter.tx(ctx, ...txes), { + 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: Array.from(new Set(txes.map((it) => it.objectClass))), - _classes: Array.from(new Set(txes.map((it) => it._class))) + classes, + _classes }) if (Array.isArray(r)) { @@ -102,6 +118,7 @@ export class DomainTxMiddleware extends BaseMiddleware implements Middleware { } } + const domains = new Set() for (const tx of txes) { const txCUD = TxProcessor.extractTx(tx) as TxCUD if (!TxProcessor.isExtendsCUD(txCUD._class)) { @@ -110,16 +127,26 @@ export class DomainTxMiddleware extends BaseMiddleware implements Middleware { continue } const domain = this.context.hierarchy.getDomain(txCUD.objectClass) + domains.add(domain) + const adapterName = this.adapterManager.getAdapterName(domain) - let group = domainGroups.get(domain) + let group = adapterGroups.get(adapterName) if (group === undefined) { group = [] - domainGroups.set(domain, group) + adapterGroups.set(adapterName, group) } group.push(txCUD) } - for (const [domain, txes] of domainGroups.entries()) { - await routeToAdapter(domain, txes) + + // 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/liveQuery.ts b/server/middleware/src/liveQuery.ts index e75a1c0dd0..4b6d91717e 100644 --- a/server/middleware/src/liveQuery.ts +++ b/server/middleware/src/liveQuery.ts @@ -89,9 +89,7 @@ export class LiveQueryMiddleware extends BaseMiddleware implements Middleware { } async tx (ctx: MeasureContext, tx: Tx[]): Promise { - for (const _tx of tx) { - await this.liveQuery.tx(_tx) - } + await this.liveQuery.tx(...tx) return await this.provideTx(ctx, tx) } } diff --git a/server/middleware/src/queryJoin.ts b/server/middleware/src/queryJoin.ts index 6e157ad20e..891de6008c 100644 --- a/server/middleware/src/queryJoin.ts +++ b/server/middleware/src/queryJoin.ts @@ -54,8 +54,9 @@ export class QueryJoiner { } if (q.result instanceof Promise) { q.result = await q.result - q.callbacks-- } + q.callbacks-- + this.removeFromQueue(q) return q.result as FindResult diff --git a/server/middleware/src/tests/queryJoiner.spec.ts b/server/middleware/src/tests/queryJoiner.spec.ts new file mode 100644 index 0000000000..e6402ec15f --- /dev/null +++ b/server/middleware/src/tests/queryJoiner.spec.ts @@ -0,0 +1,25 @@ +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(findT) + const ctx = new MeasureMetricsContext('test', {}) + + const p1 = joiner.findAll(ctx, core.class.Class, {}) + const p2 = joiner.findAll(ctx, core.class.Class, {}) + await Promise.all([p1, p2]) + expect(count).toBe(1) + expect((joiner as any).queries.size).toBe(1) + expect((joiner as any).queries.get(core.class.Class).length).toBe(0) + }) +}) diff --git a/server/middleware/src/triggers.ts b/server/middleware/src/triggers.ts index 175afc122d..de1f16ab92 100644 --- a/server/middleware/src/triggers.ts +++ b/server/middleware/src/triggers.ts @@ -51,6 +51,7 @@ import type { } from '@hcengineering/server-core' import serverCore, { BaseMiddleware, SessionDataImpl, SessionFindAll, Triggers } from '@hcengineering/server-core' import { filterBroadcastOnly } from './utils' +import { QueryJoiner } from './queryJoin' /** * @public @@ -106,13 +107,14 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { } private async processDerived (ctx: MeasureContext, txes: Tx[]): Promise { - const findAll: SessionFindAll = async (ctx, _class, query, options) => { + 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) => { @@ -121,6 +123,12 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { results.total ) } + const joiner = new QueryJoiner(_findAll) + + const findAll: SessionFindAll = async (ctx, _class, query, options) => { + return await joiner.findAll(ctx, _class, query, options) + } + 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)) @@ -223,6 +231,10 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { this.context.modelDb ) ctx.contextData = asyncContextData + + if (!((ctx as MeasureContext).contextData.isAsyncContext ?? false)) { + ctx.id = generateId() + } const aresult = await this.triggers.apply( ctx, txes, @@ -236,16 +248,15 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { ) if (aresult.length > 0) { - await ctx.with('process-aync-result', {}, async (ctx) => { - ctx.id = generateId() + 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) - if (ctx.onEnd !== undefined) { - await ctx.onEnd(ctx) - } }) } + if (ctx.onEnd !== undefined) { + await ctx.onEnd(ctx) + } } private async processDerivedTxes (ctx: MeasureContext, derived: Tx[]): Promise { diff --git a/services/github/server-github-model/src/index.ts b/services/github/server-github-model/src/index.ts index c1d6c06dbe..cebd56f8b4 100644 --- a/services/github/server-github-model/src/index.ts +++ b/services/github/server-github-model/src/index.ts @@ -14,6 +14,7 @@ export { serverGithubId } from '@hcengineering/server-github' export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverGithub.trigger.OnProjectChanges, + arrays: true, isAsync: true }) diff --git a/services/github/server-github-resources/src/index.ts b/services/github/server-github-resources/src/index.ts index 52bfc6d968..697b0c8e38 100644 --- a/services/github/server-github-resources/src/index.ts +++ b/services/github/server-github-resources/src/index.ts @@ -29,52 +29,60 @@ import tracker from '@hcengineering/tracker' /** * @public */ -export async function OnProjectChanges (tx: Tx, control: TriggerControl): Promise { - const ltx = TxProcessor.extractTx(tx) +export async function OnProjectChanges (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + const cache = new Map() - if (ltx._class === core.class.TxMixin && (ltx as TxMixin).mixin === github.mixin.GithubIssue) { - const mix = ltx as TxMixin - // Do not spend time to wait for trigger processing - await updateDocSyncInfo(control, tx, mix.objectSpace, mix) - return [] - } + const toApply: Tx[] = [] + for (const tx of txes) { + const ltx = TxProcessor.extractTx(tx) - if (control.hierarchy.isDerived(ltx._class, core.class.TxCUD)) { - const cud = ltx as TxCUD - - let space: Ref = cud.objectSpace - - if (cud._class === core.class.TxUpdateDoc) { - const upd = cud as TxUpdateDoc - if (upd.operations.space != null) { - space = upd.operations.space - } - } - - if (isDocSyncUpdateRequired(control.hierarchy, cud)) { + if (ltx._class === core.class.TxMixin && (ltx as TxMixin).mixin === github.mixin.GithubIssue) { + const mix = ltx as TxMixin // Do not spend time to wait for trigger processing - await updateDocSyncInfo(control, tx, space, cud) + await updateDocSyncInfo(control, tx, mix.objectSpace, mix, cache, toApply) + continue } - if (control.hierarchy.isDerived(cud.objectClass, time.class.ToDo)) { - if (tx._class === core.class.TxCollectionCUD) { - const coll = tx as TxCollectionCUD - if (control.hierarchy.isDerived(coll.objectClass, github.class.GithubPullRequest)) { - // Ok we got todo change for pull request, let's mark it for sync. - return [ - control.txFactory.createTxUpdateDoc( - github.class.DocSyncInfo, - coll.objectSpace, - coll.objectId as Ref, - { - needSync: '' - } + + if (TxProcessor.isExtendsCUD(ltx._class)) { + const cud = ltx as TxCUD + + let space: Ref = cud.objectSpace + + if (cud._class === core.class.TxUpdateDoc) { + const upd = cud as TxUpdateDoc + if (upd.operations.space != null) { + space = upd.operations.space + } + } + + if (isDocSyncUpdateRequired(control.hierarchy, cud)) { + await updateDocSyncInfo(control, tx, space, cud, cache, toApply) + } + if (control.hierarchy.isDerived(cud.objectClass, time.class.ToDo)) { + if (tx._class === core.class.TxCollectionCUD) { + const coll = tx as TxCollectionCUD + if (control.hierarchy.isDerived(coll.objectClass, github.class.GithubPullRequest)) { + // Ok we got todo change for pull request, let's mark it for sync. + result.push( + control.txFactory.createTxUpdateDoc( + github.class.DocSyncInfo, + coll.objectSpace, + coll.objectId as Ref, + { + needSync: '' + } + ) ) - ] + } } } } } - return [] + if (toApply.length > 0) { + await control.apply(control.ctx, toApply) + } + return result } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -108,14 +116,16 @@ async function updateDocSyncInfo ( _class: Ref> objectId: Ref objectClass: Ref> - } + }, + cache: Map, + toApply: Tx[] ): Promise { const checkTx = (tx: Tx): boolean => control.hierarchy.isDerived(tx._class, core.class.TxCUD) && (tx as TxCUD).objectClass === github.class.DocSyncInfo && (tx as TxCUD).objectId === cud.objectId - const txes = [...control.txes, ...control.ctx.contextData.broadcast.txes] + const txes = [...control.txes, ...control.ctx.contextData.broadcast.txes, ...toApply] // Check already captured Txes for (const i of txes) { if (checkTx(i)) { @@ -131,20 +141,29 @@ async function updateDocSyncInfo ( if (account === undefined) { return } + const projects = + (cache.get('projects') as GithubProject[]) ?? + (await control.queryFind(control.ctx, github.mixin.GithubProject, {}, { projection: { _id: 1 } })) + cache.set('projects', projects) - const projects = await control.queryFind(control.ctx, github.mixin.GithubProject, {}, { projection: { _id: 1 } }) if (projects.some((it) => it._id === (space as Ref))) { - const [sdoc] = await control.findAll(control.ctx, github.class.DocSyncInfo, { - _id: cud.objectId as Ref - }) + const sdoc = + (cache.get(cud.objectId) as DocSyncInfo) ?? + ( + await control.findAll(control.ctx, github.class.DocSyncInfo, { + _id: cud.objectId as Ref + }) + ).shift() + // We need to check if sync doc is already exists. if (sdoc === undefined) { // Created by non github integration // We need to create the doc sync info - await createSyncDoc(control, cud, tx, space) + createSyncDoc(control, cud, tx, space, toApply) } else { + cache.set(cud.objectId, sdoc) // We need to create the doc sync info - await updateSyncDoc(control, cud, space, sdoc) + updateSyncDoc(control, cud, space, sdoc, toApply) } } } @@ -160,7 +179,7 @@ function isDocSyncUpdateRequired (h: Hierarchy, coll: TxCUD): boolean { ) } -async function updateSyncDoc ( +function updateSyncDoc ( control: TriggerControl, cud: { _class: Ref> @@ -168,8 +187,9 @@ async function updateSyncDoc ( objectClass: Ref> }, space: Ref, - info: DocSyncInfo -): Promise { + info: DocSyncInfo, + toApply: Tx[] +): void { const data: DocumentUpdate = cud._class === core.class.TxRemoveDoc ? { @@ -183,14 +203,14 @@ async function updateSyncDoc ( data.externalVersion = '#' // We need to put this one to handle new documents.) data.space = space } - await control.apply(control.ctx, [ + toApply.push( control.txFactory.createTxUpdateDoc( github.class.DocSyncInfo, info.space, cud.objectId as Ref, data ) - ]) + ) control.ctx.contextData.broadcast.targets.github = (it) => { if (control.hierarchy.isDerived(it._class, core.class.TxCUD)) { @@ -201,7 +221,7 @@ async function updateSyncDoc ( } } -async function createSyncDoc ( +function createSyncDoc ( control: TriggerControl, cud: { _class: Ref> @@ -209,8 +229,9 @@ async function createSyncDoc ( objectClass: Ref> }, tx: Tx, - space: Ref -): Promise { + space: Ref, + toApply: Tx[] +): void { const data: DocumentUpdate = { url: '', githubNumber: 0, @@ -226,14 +247,14 @@ async function createSyncDoc ( data.attachedTo = coll.objectId } - await control.apply(control.ctx, [ + toApply.push( control.txFactory.createTxCreateDoc( github.class.DocSyncInfo, space, data, cud.objectId as Ref ) - ]) + ) control.ctx.contextData.broadcast.targets.github = (it) => { if (control.hierarchy.isDerived(it._class, core.class.TxCUD)) { if ((it as TxCUD).objectClass === github.class.DocSyncInfo) { diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml index 57800f73ca..76ba55ca6e 100644 --- a/tests/docker-compose.yaml +++ b/tests/docker-compose.yaml @@ -56,6 +56,7 @@ services: - STORAGE_CONFIG=${STORAGE_CONFIG} - MODEL_ENABLED=* - BRANDING_PATH=/var/cfg/branding.json + - STATS_URL=http://stats:4901 workspace: image: hardcoreeng/workspace links: @@ -72,6 +73,7 @@ services: - MODEL_ENABLED=* - ACCOUNTS_URL=http://account:3003 - BRANDING_PATH=/var/cfg/branding.json + - STATS_URL=http://stats:4901 restart: unless-stopped front: image: hardcoreeng/front @@ -100,6 +102,8 @@ services: - COLLABORATOR_URL=ws://localhost:3079 - STORAGE_CONFIG=${STORAGE_CONFIG} - BRANDING_URL=http://localhost:8083/branding-test.json + - STATS_URL=http://stats:4901 + - STATS_API=http://localhost:4901 transactor: image: hardcoreeng/transactor pull_policy: never @@ -128,6 +132,7 @@ services: - LAST_NAME_FIRST=true - BRANDING_PATH=/var/cfg/branding.json - FULLTEXT_URL=http://fulltext:4710 + - STATS_URL=http://stats:4901 collaborator: image: hardcoreeng/collaborator links: @@ -142,20 +147,18 @@ services: - ACCOUNTS_URL=http://account:3003 - STORAGE_CONFIG=${STORAGE_CONFIG} - FULLTEXT_URL=http://fulltext:4710 + - STATS_URL=http://stats:4901 restart: unless-stopped rekoni: image: hardcoreeng/rekoni-service restart: on-failure ports: - 4007:4004 - deploy: - resources: - limits: - memory: 1024M + environment: + - STATS_URL=http://stats:4901 + fulltext: image: hardcoreeng/fulltext - extra_hosts: - - "host.docker.internal:host-gateway" restart: unless-stopped links: - elastic @@ -172,4 +175,13 @@ services: - ELASTIC_INDEX_NAME=local_storage_index - STORAGE_CONFIG=${STORAGE_CONFIG} - REKONI_URL=http://rekoni:4007 - - ACCOUNTS_URL=http://account:3003 \ No newline at end of file + - ACCOUNTS_URL=http://account:3003 + - STATS_URL=http://stats:4901 + stats: + image: hardcoreeng/stats + ports: + - 4901:4901 + environment: + - PORT=4901 + - SERVER_SECRET=secret + restart: unless-stopped \ No newline at end of file diff --git a/tests/prepare.sh b/tests/prepare.sh index 8f9282c84f..ebb434f584 100755 --- a/tests/prepare.sh +++ b/tests/prepare.sh @@ -23,6 +23,8 @@ fi # Create user record in accounts ./tool.sh create-account user1 -f John -l Appleseed -p 1234 ./tool.sh create-account user2 -f Kainin -l Dirak -p 1234 +./tool.sh create-account super -f Super -l User -p 1234 +./tool.sh set-user-admin super true ./tool.sh assign-workspace user1 sanity-ws ./tool.sh assign-workspace user2 sanity-ws ./tool.sh set-user-role user1 sanity-ws OWNER diff --git a/tests/sanity/tests/inbox/inbox.spec.ts b/tests/sanity/tests/inbox/inbox.spec.ts index aba43b1668..2ea4dbb9a8 100644 --- a/tests/sanity/tests/inbox/inbox.spec.ts +++ b/tests/sanity/tests/inbox/inbox.spec.ts @@ -253,29 +253,40 @@ test.describe('Inbox tests', () => { const linkText = await page.locator('.antiPopup .link').textContent() const page2 = await browser.newPage() try { - const leftSideMenuPageSecond = new LeftSideMenuPage(page2) - const inboxPageSecond = new InboxPage(page2) + const channelPage2 = new ChannelPage(page2) + const leftSideMenuPage2 = new LeftSideMenuPage(page2) + const inboxPage2 = new InboxPage(page2) await leftSideMenuPage.clickOnCloseInvite() await page2.goto(linkText ?? '') - const joinPage = new SignInJoinPage(page2) - await joinPage.join(newUser2) + + const joinPage2 = new SignInJoinPage(page2) + await joinPage2.join(newUser2) + + await leftSideMenuPage2.clickChunter() + await channelPage2.clickChannel('general') await leftSideMenuPage.clickChunter() await channelPage.clickChannel('general') await channelPage.sendMessage('Test message') + + await leftSideMenuPage2.clickNotification() + await inboxPage2.clickOnInboxFilter('Channels') + await leftSideMenuPage.clickTracker() const newIssue = createNewIssueData(newUser2.firstName, newUser2.lastName) await prepareNewIssueWithOpenStep(page, newIssue, false) await issuesDetailsPage.checkIssue(newIssue) - await leftSideMenuPageSecond.clickTracker() - await leftSideMenuPageSecond.clickNotification() - await inboxPageSecond.clickOnInboxFilter('Channels') - await inboxPageSecond.checkIfInboxChatExists(newIssue.title, false) - await inboxPageSecond.checkIfInboxChatExists('Test message', true) - await inboxPageSecond.clickOnInboxFilter('Issues') - await inboxPageSecond.checkIfIssueIsPresentInInbox(newIssue.title) - await inboxPageSecond.checkIfInboxChatExists('Channel general', false) + + await leftSideMenuPage2.clickTracker() + await leftSideMenuPage2.clickNotification() + + await inboxPage2.clickOnInboxFilter('Channels') + await inboxPage2.checkIfInboxChatExists(newIssue.title, false) + await inboxPage2.checkIfInboxChatExists('Test message', true) + await inboxPage2.clickOnInboxFilter('Issues') + await inboxPage2.checkIfIssueIsPresentInInbox(newIssue.title) + await inboxPage2.checkIfInboxChatExists('Channel general', false) } finally { await page2.close() }