From e0ca033405b728646c55379eff160333441b29b6 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Wed, 13 Nov 2024 00:13:11 +0700 Subject: [PATCH 1/5] 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() } From 76bcd3c52c0a5913a6e3c31a1a202ac8b00c1b50 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Tue, 12 Nov 2024 22:40:15 +0500 Subject: [PATCH 2/5] Improve calendar schema (#7156) Signed-off-by: Denis Bykhov --- models/calendar/src/index.ts | 3 +- models/calendar/src/migration.ts | 12 +++- .../src/components/review/CreateReview.svelte | 8 +-- server/postgres/migrations/calendarSchema.sql | 5 ++ server/postgres/migrations/eventSchema.sql | 19 ++++++ server/postgres/migrations/timeSchema.sql | 17 ++++++ server/postgres/src/schemas.ts | 60 +++++++++++++++++++ server/postgres/src/storage.ts | 32 ++++++---- server/postgres/src/utils.ts | 22 ++++--- 9 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 server/postgres/migrations/calendarSchema.sql create mode 100644 server/postgres/migrations/eventSchema.sql create mode 100644 server/postgres/migrations/timeSchema.sql diff --git a/models/calendar/src/index.ts b/models/calendar/src/index.ts index b2c9099059..d5ab86ce8d 100644 --- a/models/calendar/src/index.ts +++ b/models/calendar/src/index.ts @@ -68,6 +68,7 @@ export { calendarId } from '@hcengineering/calendar' export { calendarOperation } from './migration' export const DOMAIN_CALENDAR = 'calendar' as Domain +export const DOMAIN_EVENT = 'event' as Domain @Model(calendar.class.Calendar, core.class.Doc, DOMAIN_CALENDAR) @UX(calendar.string.Calendar, calendar.icon.Calendar) @@ -85,7 +86,7 @@ export class TExternalCalendar extends TCalendar implements ExternalCalendar { externalUser!: string } -@Model(calendar.class.Event, core.class.AttachedDoc, DOMAIN_CALENDAR) +@Model(calendar.class.Event, core.class.AttachedDoc, DOMAIN_EVENT) @UX(calendar.string.Event, calendar.icon.Calendar) export class TEvent extends TAttachedDoc implements Event { declare space: Ref diff --git a/models/calendar/src/migration.ts b/models/calendar/src/migration.ts index de39d23ec4..47b3d13706 100644 --- a/models/calendar/src/migration.ts +++ b/models/calendar/src/migration.ts @@ -24,7 +24,7 @@ import { type MigrationUpgradeClient } from '@hcengineering/model' import { DOMAIN_SPACE } from '@hcengineering/model-core' -import { DOMAIN_CALENDAR } from '.' +import { DOMAIN_CALENDAR, DOMAIN_EVENT } from '.' import calendar from './plugin' async function migrateCalendars (client: MigrationClient): Promise { @@ -136,6 +136,16 @@ export const calendarOperation: MigrateOperation = { { state: 'migrate_calendars', func: migrateCalendars + }, + { + state: 'move-events', + func: async (client) => { + await client.move( + DOMAIN_CALENDAR, + { _class: { $in: client.hierarchy.getDescendants(calendar.class.Event) } }, + DOMAIN_EVENT + ) + } } ]) }, diff --git a/plugins/recruit-resources/src/components/review/CreateReview.svelte b/plugins/recruit-resources/src/components/review/CreateReview.svelte index b74bef5342..b00d4ab55a 100644 --- a/plugins/recruit-resources/src/components/review/CreateReview.svelte +++ b/plugins/recruit-resources/src/components/review/CreateReview.svelte @@ -13,7 +13,7 @@ // limitations under the License. --> + +
+
+ +
+ + + +
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 135709bed7..53e08dd656 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -272,6 +272,7 @@ export { default as TimeZonesPopup } from './components/TimeZonesPopup.svelte' export { default as CodeForm } from './components/CodeForm.svelte' export { default as CodeInput } from './components/CodeInput.svelte' export { default as TimeLeft } from './components/TimeLeft.svelte' +export { default as SectionEmpty } from './components/SectionEmpty.svelte' export { default as Dock } from './components/Dock.svelte' diff --git a/plugins/test-management-assets/.eslintrc.js b/plugins/test-management-assets/.eslintrc.js new file mode 100644 index 0000000000..e73094bc7e --- /dev/null +++ b/plugins/test-management-assets/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/plugins/test-management-assets/assets/icons.svg b/plugins/test-management-assets/assets/icons.svg new file mode 100644 index 0000000000..961c3705a9 --- /dev/null +++ b/plugins/test-management-assets/assets/icons.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/test-management-assets/config/rig.json b/plugins/test-management-assets/config/rig.json new file mode 100644 index 0000000000..b75800b9b7 --- /dev/null +++ b/plugins/test-management-assets/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "assets" +} diff --git a/plugins/test-management-assets/jest.config.js b/plugins/test-management-assets/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/plugins/test-management-assets/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/plugins/test-management-assets/lang/en.json b/plugins/test-management-assets/lang/en.json new file mode 100644 index 0000000000..135650b31c --- /dev/null +++ b/plugins/test-management-assets/lang/en.json @@ -0,0 +1,66 @@ +{ + "string": { + "ConfigLabel": "Test Management", + "ConfigDescription": "Extension to manage test cases", + "TestCaseType": "Type", + "TestCasePriority": "Priority", + "TestCaseStatus": "Status", + "TestSuite": "Test Suite", + "SuiteName": "Name", + "SuiteDescription": "Description", + "Suite": "Suite", + "TestName": "Name", + "TestDescription": "Description", + "TestType": "Type", + "TestPriority": "Priority", + "TestStatus": "Status", + "TestEstimatedTime": "Estimated time", + "TestPreconditions": "Preconditions", + "TestSteps": "Steps", + "TestAssignee": "Assignee", + "TestCase": "Test case", + "TestProject": "Project", + "TestManagementApplication": "Test Management", + "AllTestCases": "All test cases", + "AllProjects": "All projects", + "Projects": "Projects", + "CreateProject": "Create project", + "EditProject": "Edit project", + "TestCases": "Test cases", + "TestManagementDescription": "Extension to manage test cases", + "CreateTestCase": "New test case", + "FullDescription": "Description", + "ProjectName": "Name", + "ProjectType": "Type", + "Members": "Members", + "RoleLabel": "Role", + "ProjectMembers": "Project members", + "TestSuites": "Test suites", + "CreateTestSuite": "Create test suite", + "NamePlaceholder": "Suite name", + "DescriptionPlaceholder": "Description (optional)", + "TestRuns": "Test runs", + "NewTestRun": "New test run", + "TestRun": "Test run", + "TestNamePlaceholder": "Test case title", + "ChooseIcon": "Choose icon", + "NoTestSuite": "No test suite", + "StatusDraft": "Draft", + "StatusReview": "Ready for review", + "StatusReviewComments": "Need to fix review comments", + "StatusApproved": "Approved", + "StatusRejected": "Rejected", + "SetStatus": "Set status", + "Assignee": "Assignee", + "Unassigned": "Unassigned", + "AssignTo": "Assign to", + "AssignedTo": "Assigneed to", + "PreviousAssigned": "Previously assigned", + "NoTestCases": "There are no test cases in this test suite", + "CreateTestRun": "Create test run", + "TestRunNamePlaceholder": "Test run name", + "SelectTestSuites": "Select test suites", + "SelectTestCases": "Select test cases", + "TestLibrary": "Test library" + } +} diff --git a/plugins/test-management-assets/lang/fr.json b/plugins/test-management-assets/lang/fr.json new file mode 100644 index 0000000000..c2f3353047 --- /dev/null +++ b/plugins/test-management-assets/lang/fr.json @@ -0,0 +1,66 @@ +{ + "string": { + "ConfigLabel": "Gestion des tests", + "ConfigDescription": "Extension pour gérer les cas de tests", + "TestCaseType": "Taper", + "TestCasePriority": "Priorité", + "TestCaseStatus": "Statut", + "TestSuite": "Suite de tests", + "SuiteName": "Nom", + "SuiteDescription": "Description", + "Suite": "Suite", + "TestName": "Nom", + "TestDescription": "Description", + "TestType": "Taper", + "TestPriority": "Priorité", + "TestStatus": "Statut", + "TestEstimatedTime": "Temps estimé", + "TestPreconditions": "Conditions préalables", + "TestSteps": "Mesures", + "TestAssignee": "Cessionnaire", + "TestCase": "Cas de test", + "TestProject": "Projet", + "TestManagementApplication": "Gestion des tests", + "AllTestCases": "Tous les cas de tests", + "AllProjects": "Tous les projets", + "Projects": "Projets", + "CreateProject": "Créer un projet", + "EditProject": "Modifier le projet", + "TestCases": "Cas de tests", + "TestManagementDescription": "Extension pour gérer les cas de tests", + "CreateTestCase": "Nouveau cas de test", + "FullDescription": "Description", + "ProjectName": "Nom", + "ProjectType": "Taper", + "Members": "Membres", + "RoleLabel": "Rôle", + "ProjectMembers": "Membres du projet", + "TestSuites": "Suites de tests", + "CreateTestSuite": "Créer une suite de tests", + "NamePlaceholder": "Nom de la suite", + "DescriptionPlaceholder": "Description (facultatif)", + "TestRuns": "Exécutions de tests", + "NewTestRun": "Nouveau test", + "TestRun": "Exécution d'essai", + "TestNamePlaceholder": "Titre du scénario de test", + "ChooseIcon": "Choisir l'icône", + "NoTestSuite": "Pas de suite de tests", + "StatusDraft": "Brouillon", + "StatusReview": "Prêt pour l'examen", + "StatusReviewComments": "Besoin de corriger les commentaires d'évaluation", + "StatusApproved": "Approuvé", + "StatusRejected": "Rejeté", + "SetStatus": "Définir le statut", + "Assignee": "Attribué à", + "Unassigned": "Non assigné", + "AssignTo": "Assigner à...", + "AssignedTo": "Assigné à {value}", + "PreviousAssigned": "Assigné précédemment", + "NoTestCases": "Il n'y a aucun cas de test dans cette suite de tests", + "CreateTestRun": "Créer une série de tests", + "TestRunNamePlaceholder": "Nom de l'exécution du test", + "SelectTestSuites": "Sélectionnez les suites de tests", + "SelectTestCases": "Sélectionnez des cas de test", + "TestLibrary": "Bibliothèque de tests" + } +} diff --git a/plugins/test-management-assets/lang/ru.json b/plugins/test-management-assets/lang/ru.json new file mode 100644 index 0000000000..19a45e0d6c --- /dev/null +++ b/plugins/test-management-assets/lang/ru.json @@ -0,0 +1,66 @@ +{ + "string": { + "ConfigLabel": "Управление тестированием", + "ConfigDescription": "Расширение для управления тестирование", + "TestCaseType": "Тип", + "TestCasePriority": "Приоритет", + "TestCaseStatus": "Статус", + "TestSuite": "Тестовый набор", + "SuiteName": "Имя", + "SuiteDescription": "Описание", + "Suite": "Тестовый набор", + "TestName": "Имя", + "TestDescription": "Описание", + "TestType": "Тип", + "TestPriority": "Приоритет", + "TestStatus": "Статус", + "TestEstimatedTime": "Расчетное время", + "TestPreconditions": "Предварительные условия", + "TestSteps": "Шаги", + "TestAssignee": "Исполнитель", + "TestCase": "Тест-кейс", + "TestProject": "Проект", + "TestManagementApplication": "Управление тестированием", + "AllTestCases": "Все тест-кейсы", + "AllProjects": "Все проекты", + "Projects": "Проекты", + "CreateProject": "Создать проект", + "EditProject": "Отредактировать проект", + "TestCases": "Тест-кейсы", + "TestManagementDescription": "Расширение для управления тестирование", + "CreateTestCase": "Новый тест-кейс", + "FullDescription": "Описание", + "ProjectName": "Имя", + "ProjectType": "Тип", + "Members": "Участники", + "RoleLabel": "Роль", + "ProjectMembers": "Участники проекта", + "TestSuites": "Тестовые наборы", + "CreateTestSuite": "Создать тестовый набор", + "NamePlaceholder": "Имя тестового набора", + "DescriptionPlaceholder": "Описание (опционально)", + "TestRuns": "Выполнение тестов", + "NewTestRun": "Новый тест план", + "TestRun": "Тест план", + "TestNamePlaceholder": "Имя тест кейса", + "ChooseIcon": "Выберите иконку", + "NoTestSuite": "Тестовый набор не задан", + "StatusDraft": "В прогрессе", + "StatusReview": "Готов для ревью", + "StatusReviewComments": "Требует исправлений", + "StatusApproved": "Согласован", + "StatusRejected": "Отклонен", + "SetStatus": "Выбрать статус", + "Assignee": "Исполнитель", + "Unassigned": "Не назначен", + "AssignTo": "Назначить на", + "AssignedTo": "Назначено на", + "PreviousAssigned": "Ранее назначенные", + "NoTestCases": "Тест-кейсы отсутствует", + "CreateTestRun": "Создать тест план", + "TestRunNamePlaceholder": "Название тест плана", + "SelectTestSuites": "Выбрать наборы тестов", + "SelectTestCases": "Выбрать тест-кейсы", + "TestLibrary": "Библиотека тестов" + } +} diff --git a/plugins/test-management-assets/lang/zh.json b/plugins/test-management-assets/lang/zh.json new file mode 100644 index 0000000000..c7da64156d --- /dev/null +++ b/plugins/test-management-assets/lang/zh.json @@ -0,0 +1,66 @@ +{ + "string": { + "ConfigLabel": "測試管理", + "ConfigDescription": "管理測試用例的擴展", + "TestCaseType": "類型", + "TestCasePriority": "優先事項", + "TestCaseStatus": "地位", + "TestSuite": "測試套件", + "SuiteName": "姓名", + "SuiteDescription": "描述", + "Suite": "Suite", + "TestName": "姓名", + "TestDescription": "描述", + "TestType": "類型", + "TestPriority": "優先事項", + "TestStatus": "地位", + "TestEstimatedTime": "預計時間", + "TestPreconditions": "前提條件", + "TestSteps": "步驟", + "TestAssignee": "受讓人", + "TestCase": "測試用例", + "TestProject": "專案", + "TestManagementApplication": "測試管理", + "AllTestCases": "所有測試用例", + "AllProjects": "所有項目", + "Projects": "專案", + "CreateProject": "創建專案", + "EditProject": "編輯項目", + "TestCases": "測試用例", + "TestManagementDescription": "管理測試用例的擴展", + "CreateTestCase": "新測試用例", + "FullDescription": "描述", + "ProjectName": "姓名", + "ProjectType": "類型", + "Members": "會員", + "RoleLabel": "角色", + "ProjectMembers": "專案成員", + "TestSuites": "測試套件", + "CreateTestSuite": "建立測試套件", + "NamePlaceholder": "套房名稱", + "DescriptionPlaceholder": "說明(可選)", + "TestRuns": "試運行", + "NewTestRun": "新試運行", + "TestRun": "試運行", + "TestNamePlaceholder": "測試用例標題", + "ChooseIcon": "選擇圖示", + "NoTestSuite": "沒有測試套件", + "StatusDraft": "草稿", + "StatusReview": "準備審查", + "StatusReviewComments": "需要修復審核意見", + "StatusApproved": "得到正式認可的", + "StatusRejected": "被拒絕", + "SetStatus": "設定狀態", + "Assignee": "受讓人", + "Unassigned": "未分配", + "AssignTo": "分配給", + "AssignedTo": "分配給", + "PreviousAssigned": "先前分配的", + "NoTestCases": "該測試套件中沒有測試案例", + "CreateTestRun": "建立測試運行", + "TestRunNamePlaceholder": "測試運行名稱", + "SelectTestSuites": "選擇測試套件", + "SelectTestCases": "選擇測試用例", + "TestLibrary": "測試庫" + } +} diff --git a/plugins/test-management-assets/package.json b/plugins/test-management-assets/package.json new file mode 100644 index 0000000000..f4e58bf2ee --- /dev/null +++ b/plugins/test-management-assets/package.json @@ -0,0 +1,39 @@ +{ + "name": "@hcengineering/test-management-assets", + "version": "0.6.0", + "main": "src/index.ts", + "author": "Anticrm Platform Contributors", + "template": "@hcengineering/assets-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent", + "build:docs": "", + "format": "format src", + "build:watch": "compile", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "eslint": "^8.54.0", + "prettier": "^3.1.0", + "@types/node": "~20.11.16", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/platform": "^0.6.11", + "@hcengineering/test-management": "^0.6.0" + } +} diff --git a/plugins/test-management-assets/src/__tests__/lang.test.ts b/plugins/test-management-assets/src/__tests__/lang.test.ts new file mode 100644 index 0000000000..cf96c6dcf7 --- /dev/null +++ b/plugins/test-management-assets/src/__tests__/lang.test.ts @@ -0,0 +1,6 @@ +import { makeLocalesTest } from '@hcengineering/platform' + +it( + 'Locales are equale', + makeLocalesTest((lang) => import(`../../lang/${lang}.json`)) +) diff --git a/plugins/test-management-assets/src/index.ts b/plugins/test-management-assets/src/index.ts new file mode 100644 index 0000000000..5498b02acc --- /dev/null +++ b/plugins/test-management-assets/src/index.ts @@ -0,0 +1,40 @@ +// +// 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 testManagement from '@hcengineering/test-management' +import { loadMetadata } from '@hcengineering/platform' + +const icons = require('../assets/icons.svg') as string // eslint-disable-line +loadMetadata(testManagement.icon, { + TestCase: `${icons}#testCase`, + TestManagementApplication: `${icons}#testManagementApplication`, + TestManagement: `${icons}#testCase`, + TestManagementVersion: `${icons}#testCase`, + TestCases: `${icons}#testCase`, + Home: `${icons}#home`, + Estimation: `${icons}#testCase`, + TestSuite: `${icons}#testSuite`, + TestProject: `${icons}#testCase`, + TestSuites: `${icons}#testSuite`, + TestRuns: `${icons}#testRun`, + RedCircle: `${icons}#red-circle`, + Document: `${icons}#document`, + StatusDraft: `${icons}#status-draft`, + StatusReview: `${icons}#status-review`, + StatusReviewComments: `${icons}#status-review-comments`, + StatusApproved: `${icons}#status-approved`, + StatusRejected: `${icons}#status-canceled`, + TestLibrary: `${icons}#test-library` +}) diff --git a/plugins/test-management-assets/tsconfig.json b/plugins/test-management-assets/tsconfig.json new file mode 100644 index 0000000000..4449c73536 --- /dev/null +++ b/plugins/test-management-assets/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/assets/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "types": ["node", "jest"], + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/plugins/test-management-resources/.eslintrc.js b/plugins/test-management-resources/.eslintrc.js new file mode 100644 index 0000000000..bb8fd7450d --- /dev/null +++ b/plugins/test-management-resources/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'], + parserOptions: { tsconfigRootDir: __dirname } +} diff --git a/plugins/test-management-resources/.prettierrc b/plugins/test-management-resources/.prettierrc new file mode 100644 index 0000000000..792942803a --- /dev/null +++ b/plugins/test-management-resources/.prettierrc @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "trailingComma": "none", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 120, + "useTabs": false, + "bracketSpacing": true, + "proseWrap": "preserve", + "plugins": [ + "prettier-plugin-svelte" + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} \ No newline at end of file diff --git a/plugins/test-management-resources/config/rig.json b/plugins/test-management-resources/config/rig.json new file mode 100644 index 0000000000..bcad6f7c33 --- /dev/null +++ b/plugins/test-management-resources/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "ui" +} diff --git a/plugins/test-management-resources/jest.config.js b/plugins/test-management-resources/jest.config.js new file mode 100644 index 0000000000..3656e284d3 --- /dev/null +++ b/plugins/test-management-resources/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'] +} diff --git a/plugins/test-management-resources/package.json b/plugins/test-management-resources/package.json new file mode 100644 index 0000000000..7cec2be751 --- /dev/null +++ b/plugins/test-management-resources/package.json @@ -0,0 +1,77 @@ +{ + "name": "@hcengineering/test-management-resources", + "version": "0.6.0", + "main": "src/index.ts", + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile ui", + "build:docs": "api-extractor run --local", + "svelte-check": "do-svelte-check", + "_phase:svelte-check": "do-svelte-check", + "format": "format src", + "build:watch": "compile ui", + "_phase:build": "compile ui", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "svelte-loader": "^3.2.0", + "sass": "^1.53.0", + "svelte-preprocess": "^5.1.3", + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-svelte": "^2.35.1", + "prettier-plugin-svelte": "^3.2.2", + "eslint": "^8.54.0", + "prettier": "^3.1.0", + "svelte-check": "^3.6.9", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "svelte-eslint-parser": "^0.33.1" + }, + "dependencies": { + "@hcengineering/activity": "^0.6.0", + "@hcengineering/activity-resources": "^0.6.1", + "@hcengineering/analytics": "^0.6.0", + "@hcengineering/attachment": "^0.6.14", + "@hcengineering/attachment-resources": "^0.6.0", + "@hcengineering/calendar": "^0.6.24", + "@hcengineering/chunter": "^0.6.20", + "@hcengineering/chunter-resources": "^0.6.0", + "@hcengineering/client": "^0.6.18", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/contact-resources": "^0.6.0", + "@hcengineering/core": "^0.6.32", + "@hcengineering/kanban": "^0.6.0", + "@hcengineering/login": "^0.6.12", + "@hcengineering/notification": "^0.6.23", + "@hcengineering/notification-resources": "^0.6.0", + "@hcengineering/panel": "^0.6.23", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/preference": "^0.6.13", + "@hcengineering/presentation": "^0.6.3", + "@hcengineering/query": "^0.6.12", + "@hcengineering/setting": "^0.6.17", + "@hcengineering/tags": "^0.6.16", + "@hcengineering/task": "^0.6.20", + "@hcengineering/task-resources": "^0.6.0", + "@hcengineering/text-editor-resources": "^0.6.0", + "@hcengineering/text": "^0.6.5", + "@hcengineering/test-management": "^0.6.0", + "@hcengineering/ui": "^0.6.15", + "@hcengineering/view": "^0.6.13", + "@hcengineering/view-resources": "^0.6.0", + "@hcengineering/workbench": "^0.6.16", + "@hcengineering/workbench-resources": "^0.6.1", + "fast-equals": "^5.0.1", + "svelte": "^4.2.19" + } +} diff --git a/plugins/test-management-resources/postcss.config.js b/plugins/test-management-resources/postcss.config.js new file mode 100644 index 0000000000..88752c6cb0 --- /dev/null +++ b/plugins/test-management-resources/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + require('autoprefixer') + ] +} diff --git a/plugins/test-management-resources/src/components/icons/FileDuo.svelte b/plugins/test-management-resources/src/components/icons/FileDuo.svelte new file mode 100644 index 0000000000..71f6f606b2 --- /dev/null +++ b/plugins/test-management-resources/src/components/icons/FileDuo.svelte @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/plugins/test-management-resources/src/components/project/CreateProject.svelte b/plugins/test-management-resources/src/components/project/CreateProject.svelte new file mode 100644 index 0000000000..488f393ab0 --- /dev/null +++ b/plugins/test-management-resources/src/components/project/CreateProject.svelte @@ -0,0 +1,390 @@ + + + + +
+
+
+
+ + +
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + {#each roles as role} +
+
+
+ { + handleRoleAssignmentChanged(role._id, refs) + }} + kind={'regular'} + size={'large'} + /> +
+ {/each} +
+
diff --git a/plugins/test-management-resources/src/components/project/ProjectPresenter.svelte b/plugins/test-management-resources/src/components/project/ProjectPresenter.svelte new file mode 100644 index 0000000000..e683b907f2 --- /dev/null +++ b/plugins/test-management-resources/src/components/project/ProjectPresenter.svelte @@ -0,0 +1,29 @@ + + + +{#if value} +
+ + {value.name} + +
+{/if} diff --git a/plugins/test-management-resources/src/components/project/ProjectSpacePresenter.svelte b/plugins/test-management-resources/src/components/project/ProjectSpacePresenter.svelte new file mode 100644 index 0000000000..40dba55896 --- /dev/null +++ b/plugins/test-management-resources/src/components/project/ProjectSpacePresenter.svelte @@ -0,0 +1,105 @@ + + + +{#if specials} + getActions(space)} + {forciblyСollapsed} + > + {#each specials as special} + + + + {/each} + + + {#if visible} + {@const item = specials.find((sp) => sp.id === currentSpecial && currentSpace === space._id)} + {#if item} + + + + {/if} + {/if} + + +{/if} diff --git a/plugins/test-management-resources/src/components/test-case/AssigneeEditor.svelte b/plugins/test-management-resources/src/components/test-case/AssigneeEditor.svelte new file mode 100644 index 0000000000..ea405cd046 --- /dev/null +++ b/plugins/test-management-resources/src/components/test-case/AssigneeEditor.svelte @@ -0,0 +1,198 @@ + + + +{#if _object} + {#if isAction} + { + const result = evt.detail + if (result === null) { + handleAssigneeChanged(null) + } else if (result !== undefined && result._id !== value) { + value = result._id + handleAssigneeChanged(result._id) + } + }} + /> + {:else} + handleAssigneeChanged(detail)} + /> + {/if} +{/if} diff --git a/plugins/test-management-resources/src/components/test-case/CreateTestCase.svelte b/plugins/test-management-resources/src/components/test-case/CreateTestCase.svelte new file mode 100644 index 0000000000..aee61920a7 --- /dev/null +++ b/plugins/test-management-resources/src/components/test-case/CreateTestCase.svelte @@ -0,0 +1,200 @@ + + + + + + 0} + on:close={() => { + dispatch('close') + }} + on:changeContent +> + + + + + +
+ +
+ + { + if (ev.detail.size > 0) attachments = ev.detail.values + else if (ev.detail.size === 0 && ev.detail.values != null) { + attachments.clear() + attachments = attachments + } + }} + /> + + + (object.assignee = detail)} + /> + + + + + {#if attachments.size > 0} + {#each Array.from(attachments.values()) as attachment} + { + if (result.detail !== undefined) descriptionBox.removeAttachmentById(result.detail._id) + }} + /> + {/each} + {/if} + + + + +{:else if kind === 'list-header'} +
+ {#if shouldShowAvatar} + + {/if} + +
+{:else} +