UBERF-8582: Fix triggers (#7155)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-11-13 00:13:11 +07:00 committed by GitHub
parent a220fac255
commit e0ca033405
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 607 additions and 526 deletions

3
.vscode/launch.json vendored
View File

@ -55,7 +55,8 @@
// "VERSION": "0.6.289", // "VERSION": "0.6.289",
"ELASTIC_INDEX_NAME": "local_storage_index", "ELASTIC_INDEX_NAME": "local_storage_index",
"UPLOAD_URL": "/files", "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"], "runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20", "runtimeVersion": "20",

View File

@ -221,7 +221,6 @@ services:
links: links:
- mongodb - mongodb
- minio - minio
- rekoni
- account - account
- stats - stats
# - apm-server # - apm-server
@ -261,7 +260,6 @@ services:
links: links:
- postgres - postgres
- minio - minio
- rekoni
- account - account
- stats - stats
# - apm-server # - apm-server
@ -296,10 +294,6 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- 4004:4004 - 4004:4004
deploy:
resources:
limits:
memory: 1024M
fulltext: fulltext:
image: hardcoreeng/fulltext image: hardcoreeng/fulltext
extra_hosts: extra_hosts:
@ -319,10 +313,6 @@ services:
- STATS_URL=http://host.docker.internal:4900 - STATS_URL=http://host.docker.internal:4900
- REKONI_URL=http://host.docker.internal:4004 - REKONI_URL=http://host.docker.internal:4004
- ACCOUNTS_URL=http://host.docker.internal:3000 - ACCOUNTS_URL=http://host.docker.internal:3000
deploy:
resources:
limits:
memory: 300M
fulltext_pg: fulltext_pg:
image: hardcoreeng/fulltext image: hardcoreeng/fulltext
extra_hosts: extra_hosts:
@ -343,10 +333,6 @@ services:
- STATS_URL=http://host.docker.internal:4900 - STATS_URL=http://host.docker.internal:4900
- REKONI_URL=http://host.docker.internal:4004 - REKONI_URL=http://host.docker.internal:4004
- ACCOUNTS_URL=http://host.docker.internal:3000 - ACCOUNTS_URL=http://host.docker.internal:3000
deploy:
resources:
limits:
memory: 300M
print: print:
image: hardcoreeng/print image: hardcoreeng/print
extra_hosts: extra_hosts:
@ -358,10 +344,6 @@ services:
- SECRET=secret - SECRET=secret
- STORAGE_CONFIG=${STORAGE_CONFIG} - STORAGE_CONFIG=${STORAGE_CONFIG}
- STATS_URL=http://host.docker.internal:4900 - STATS_URL=http://host.docker.internal:4900
deploy:
resources:
limits:
memory: 300M
sign: sign:
image: hardcoreeng/sign image: hardcoreeng/sign
extra_hosts: extra_hosts:
@ -382,10 +364,6 @@ services:
- SERVICE_ID=sign-service - SERVICE_ID=sign-service
- BRANDING_PATH=/var/cfg/branding.json - BRANDING_PATH=/var/cfg/branding.json
- STATS_URL=http://host.docker.internal:4900 - STATS_URL=http://host.docker.internal:4900
deploy:
resources:
limits:
memory: 300M
analytics: analytics:
image: hardcoreeng/analytics-collector image: hardcoreeng/analytics-collector
extra_hosts: extra_hosts:
@ -402,10 +380,6 @@ services:
- ACCOUNTS_URL=http://host.docker.internal:3000 - ACCOUNTS_URL=http://host.docker.internal:3000
- SUPPORT_WORKSPACE=support - SUPPORT_WORKSPACE=support
- STATS_URL=http://host.docker.internal:4900 - STATS_URL=http://host.docker.internal:4900
deploy:
resources:
limits:
memory: 300M
aiBot: aiBot:
image: hardcoreeng/ai-bot image: hardcoreeng/ai-bot
ports: ports:
@ -426,10 +400,6 @@ services:
- STATS_URL=http://host.docker.internal:4900 - STATS_URL=http://host.docker.internal:4900
# - LOVE_ENDPOINT=http://host.docker.internal:8096 # - LOVE_ENDPOINT=http://host.docker.internal:8096
# - OPENAI_API_KEY=token # - OPENAI_API_KEY=token
deploy:
resources:
limits:
memory: 300M
# telegram-bot: # telegram-bot:
# image: hardcoreeng/telegram-bot # image: hardcoreeng/telegram-bot
# extra_hosts: # extra_hosts:
@ -445,10 +415,6 @@ services:
# - ACCOUNTS_URL=http://host.docker.internal:3000 # - ACCOUNTS_URL=http://host.docker.internal:3000
# - SERVICE_ID=telegram-bot-service # - SERVICE_ID=telegram-bot-service
# - STATS_URL=http://host.docker.internal:4900 # - STATS_URL=http://host.docker.internal:4900
# deploy:
# resources:
# limits:
# memory: 300M
volumes: volumes:
db: db:
dbpg: dbpg:

View File

@ -2,50 +2,51 @@
// Copyright @ 2022-2023 Hardcore Engineering Inc. // Copyright @ 2022-2023 Hardcore Engineering Inc.
// //
import attachment, { type Attachment } from '@hcengineering/attachment'
import { 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 Data,
type Ref, type Doc,
TxOperations,
generateId,
DOMAIN_TX, DOMAIN_TX,
generateId,
makeCollaborativeDoc, makeCollaborativeDoc,
MeasureMetricsContext, MeasureMetricsContext,
type Class, type Ref,
type Doc, SortingOrder,
SortingOrder toIdMap,
TxOperations
} from '@hcengineering/core' } from '@hcengineering/core'
import { import {
createDefaultSpace, createDefaultSpace,
createOrUpdate, createOrUpdate,
type MigrateUpdate,
type MigrationDocumentQuery,
tryMigrate,
tryUpgrade,
type MigrateOperation, type MigrateOperation,
type MigrateUpdate,
type MigrationClient, type MigrationClient,
type MigrationUpgradeClient type MigrationDocumentQuery,
type MigrationUpgradeClient,
tryMigrate,
tryUpgrade
} from '@hcengineering/model' } from '@hcengineering/model'
import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment'
import core from '@hcengineering/model-core' import core from '@hcengineering/model-core'
import tags from '@hcengineering/tags' 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' import documents, { DOMAIN_DOCUMENTS } from './index'
@ -210,6 +211,7 @@ async function createDocumentCategories (tx: TxOperations): Promise<void> {
{ code: 'CM', title: 'Client Management' } { code: 'CM', title: 'Client Management' }
] ]
const catsCache = toIdMap(await tx.findAll(documents.class.DocumentCategory, {}))
const ops = tx.apply() const ops = tx.apply()
for (const c of categories) { for (const c of categories) {
await createOrUpdate( await createOrUpdate(
@ -217,7 +219,8 @@ async function createDocumentCategories (tx: TxOperations): Promise<void> {
documents.class.DocumentCategory, documents.class.DocumentCategory,
documents.space.QualityDocuments, documents.space.QualityDocuments,
{ ...c, attachments: 0 }, { ...c, attachments: 0 },
((documents.category.DOC as string) + ' - ' + c.code) as Ref<DocumentCategory> ((documents.category.DOC as string) + ' - ' + c.code) as Ref<DocumentCategory>,
catsCache
) )
} }
await ops.commit() await ops.commit()

View File

@ -14,7 +14,15 @@
// //
import { getCategories } from '@anticrm/skillset' 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 { import {
createOrUpdate, createOrUpdate,
migrateSpace, migrateSpace,
@ -26,7 +34,7 @@ import {
type ModelLogger type ModelLogger
} from '@hcengineering/model' } from '@hcengineering/model'
import tags, { type TagCategory } from '@hcengineering/model-tags' 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 { recruitId, type Applicant } from '@hcengineering/recruit'
import { DOMAIN_CALENDAR } from '@hcengineering/model-calendar' import { DOMAIN_CALENDAR } from '@hcengineering/model-calendar'
@ -194,10 +202,16 @@ async function createDefaults (client: MigrationUpgradeClient, tx: TxOperations)
}, },
recruit.category.Other 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<Doc>(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
)
for (const c of cats) {
await createOrUpdate( await createOrUpdate(
tx, ops,
tags.class.TagCategory, tags.class.TagCategory,
core.space.Workspace, core.space.Workspace,
{ {
@ -207,9 +221,11 @@ async function createDefaults (client: MigrationUpgradeClient, tx: TxOperations)
tags: c.skills, tags: c.skills,
default: false default: false
}, },
(recruit.category.Category + '.' + c.id) as Ref<TagCategory> (recruit.category.Category + '.' + c.id) as Ref<TagCategory>,
existingCategories
) )
} }
await ops.commit()
await createSequence(tx, recruit.class.Review) await createSequence(tx, recruit.class.Review)
await createSequence(tx, recruit.class.Opinion) await createSequence(tx, recruit.class.Opinion)

View File

@ -44,14 +44,13 @@ export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverActivity.trigger.ActivityMessagesHandler, trigger: serverActivity.trigger.ActivityMessagesHandler,
txMatch: { arrays: true,
'tx.objectClass': { $nin: [activity.class.ActivityMessage, notification.class.DocNotifyContext] }
},
isAsync: true isAsync: true
}) })
builder.createDoc(serverCore.class.Trigger, core.space.Model, { 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, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {

View File

@ -37,6 +37,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverAiBot.trigger.OnMessageSend, trigger: serverAiBot.trigger.OnMessageSend,
arrays: true,
isAsync: true isAsync: true
}) })

View File

@ -23,6 +23,7 @@ export { serverCollaborationId } from '@hcengineering/server-collaboration'
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverCollaboration.trigger.OnDelete trigger: serverCollaboration.trigger.OnDelete,
arrays: true
}) })
} }

View File

@ -16,6 +16,7 @@
import { type Builder, Mixin, Model } from '@hcengineering/model' import { type Builder, Mixin, Model } from '@hcengineering/model'
import contact from '@hcengineering/contact'
import core, { type Ref } from '@hcengineering/core' import core, { type Ref } from '@hcengineering/core'
import { TClass, TDoc } from '@hcengineering/model-core' import { TClass, TDoc } from '@hcengineering/model-core'
import { TNotificationType } from '@hcengineering/model-notification' import { TNotificationType } from '@hcengineering/model-notification'
@ -33,7 +34,6 @@ import serverNotification, {
type TypeMatch, type TypeMatch,
type TypeMatchFunc type TypeMatchFunc
} from '@hcengineering/server-notification' } from '@hcengineering/server-notification'
import contact from '@hcengineering/contact'
export { serverNotificationId } from '@hcengineering/server-notification' export { serverNotificationId } from '@hcengineering/server-notification'
@ -89,7 +89,8 @@ export function createModel (builder: Builder): void {
}) })
builder.createDoc(serverCore.class.Trigger, core.space.Model, { 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, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {

View File

@ -25,7 +25,8 @@ export { serverRequestId } from '@hcengineering/server-request'
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, { 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, { builder.mixin(request.class.Request, core.class.Class, serverNotification.mixin.TextPresenter, {

View File

@ -23,7 +23,8 @@ export { serverTagsId } from '@hcengineering/server-tags'
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTags.trigger.onTagReference trigger: serverTags.trigger.onTagReference,
arrays: true
}) })
builder.mixin<Class<Doc>, ObjectDDParticipant>( builder.mixin<Class<Doc>, ObjectDDParticipant>(

View File

@ -22,6 +22,7 @@ export { serverTaskId } from '@hcengineering/server-task'
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTask.trigger.OnStateUpdate trigger: serverTask.trigger.OnStateUpdate,
arrays: true
}) })
} }

View File

@ -38,6 +38,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTime.trigger.OnTask, trigger: serverTime.trigger.OnTask,
arrays: true,
isAsync: true isAsync: true
}) })

View File

@ -15,10 +15,10 @@
import core, { type Doc } from '@hcengineering/core' import core, { type Doc } from '@hcengineering/core'
import { type Builder, Mixin } from '@hcengineering/model' 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 { TClass } from '@hcengineering/model-core'
import { type Resource } from '@hcengineering/platform' 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' export { serverViewId } from '@hcengineering/server-view'
@ -31,6 +31,10 @@ export function createModel (builder: Builder): void {
builder.createModel(TServerLinkIdProvider) builder.createModel(TServerLinkIdProvider)
builder.createDoc(serverCore.class.Trigger, core.space.Model, { 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
}
}) })
} }

View File

@ -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' import { deepEqual } from 'fast-equals'
function toUndef (value: any): any { function toUndef (value: any): any {
@ -40,9 +40,10 @@ export async function createOrUpdate<T extends Doc> (
_class: Ref<Class<T>>, _class: Ref<Class<T>>,
space: Ref<Space>, space: Ref<Space>,
data: Data<T>, data: Data<T>,
_id: Ref<T> _id: Ref<T>,
cache?: IdMap<Doc>
): Promise<void> { ): Promise<void> {
const existingDoc = await client.findOne<Doc>(_class, { _id }) const existingDoc = cache !== undefined ? cache.get(_id) : await client.findOne<Doc>(_class, { _id })
if (existingDoc !== undefined) { if (existingDoc !== undefined) {
const { _class: _oldClass, _id, space: _oldSpace, modifiedBy, modifiedOn, ...oldData } = existingDoc const { _class: _oldClass, _id, space: _oldSpace, modifiedBy, modifiedOn, ...oldData } = existingDoc
if (modifiedBy === client.txFactory.account) { if (modifiedBy === client.txFactory.account) {

View File

@ -3,11 +3,11 @@
// //
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service'
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core' import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
import { startFront } from '@hcengineering/front/src/starter' 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 { initStatisticsContext } from '@hcengineering/server-core'
import { join } from 'path'
configureAnalytics(process.env.SENTRY_DSN, {}) configureAnalytics(process.env.SENTRY_DSN, {})
Analytics.setTag('application', 'front') Analytics.setTag('application', 'front')
@ -45,5 +45,5 @@ startFront(metricsContext, {
ANALYTICS_COLLECTOR_URL: process.env.ANALYTICS_COLLECTOR_URL, ANALYTICS_COLLECTOR_URL: process.env.ANALYTICS_COLLECTOR_URL,
AI_URL: process.env.AI_URL, AI_URL: process.env.AI_URL,
TELEGRAM_BOT_URL: process.env.TELEGRAM_BOT_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
}) })

View File

@ -20,7 +20,7 @@ import { initStatisticsContext, type StorageConfiguration } from '@hcengineering
import { join } from 'path' import { join } from 'path'
import { createElasticAdapter } from '@hcengineering/elastic' 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 { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import { startIndexer } from './server' import { startIndexer } from './server'
@ -80,11 +80,6 @@ const config: FulltextDBConfiguration = {
factory: createRekoniAdapter, factory: createRekoniAdapter,
contentType: '*', contentType: '*',
url: rekoniUrl url: rekoniUrl
},
YDoc: {
factory: createYDocAdapter,
contentType: 'application/ydoc',
url: ''
} }
}, },
defaultContentAdapter: 'Rekoni' defaultContentAdapter: 'Rekoni'

View File

@ -8,8 +8,7 @@
"template": "@hcengineering/node-package", "template": "@hcengineering/node-package",
"license": "EPL-2.0", "license": "EPL-2.0",
"scripts": { "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": "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-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-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", "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", "build": "compile",
"_phase:bundle": "rushx bundle", "_phase:bundle": "rushx bundle",

View File

@ -387,59 +387,58 @@ export async function generateDocUpdateMessages (
return res return res
} }
async function ActivityMessagesHandler (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> { async function ActivityMessagesHandler (_txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
if ( const ltxes = _txes.filter(
control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage) || (it) =>
control.hierarchy.isDerived(tx.objectClass, notification.class.DocNotifyContext) || !(
control.hierarchy.isDerived(tx.objectClass, notification.class.ActivityInboxNotification) || control.hierarchy.isDerived(it.objectClass, activity.class.ActivityMessage) ||
control.hierarchy.isDerived(tx.objectClass, notification.class.BrowserNotification) control.hierarchy.isDerived(it.objectClass, notification.class.DocNotifyContext) ||
) { control.hierarchy.isDerived(it.objectClass, notification.class.ActivityInboxNotification) ||
return [] control.hierarchy.isDerived(it.objectClass, notification.class.BrowserNotification)
} )
)
const cache: DocObjectCache = control.contextCache.get('ActivityMessagesHandler') ?? { const cache: DocObjectCache = control.contextCache.get('ActivityMessagesHandler') ?? {
docs: new Map(), docs: new Map(),
transactions: new Map() transactions: new Map()
} }
control.contextCache.set('ActivityMessagesHandler', cache) control.contextCache.set('ActivityMessagesHandler', cache)
const result: Tx[] = []
for (const tx of ltxes) {
const txes = const txes =
tx.space === core.space.DerivedTx tx.space === core.space.DerivedTx
? [] ? []
: await control.ctx.with( : await control.ctx.with('generateDocUpdateMessages', {}, (ctx) =>
'generateDocUpdateMessages', generateDocUpdateMessages(ctx, tx, control, [], undefined, cache)
{},
async (ctx) => await generateDocUpdateMessages(ctx, tx, control, [], undefined, cache)
) )
const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc<DocUpdateMessage>)) const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc<DocUpdateMessage>))
const notificationTxes = await control.ctx.with( const notificationTxes = await control.ctx.with('createCollaboratorNotifications', {}, (ctx) =>
'createCollaboratorNotifications', createCollaboratorNotifications(ctx, tx, control, messages, undefined, cache.docs as Map<Ref<Doc>, Doc>)
{},
async (ctx) =>
await createCollaboratorNotifications(ctx, tx, control, messages, undefined, cache.docs as Map<Ref<Doc>, Doc>)
) )
const result = [...txes, ...notificationTxes] result.push(...txes, ...notificationTxes)
}
if (result.length > 0) { if (result.length > 0) {
await control.apply(control.ctx, result) await control.apply(control.ctx, result)
} }
return [] return []
} }
async function OnDocRemoved (originTx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> { async function OnDocRemoved (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const originTx of txes) {
const tx = TxProcessor.extractTx(originTx) as TxCUD<Doc> const tx = TxProcessor.extractTx(originTx) as TxCUD<Doc>
if (tx._class !== core.class.TxRemoveDoc) { if (tx._class !== core.class.TxRemoveDoc) {
return [] continue
} }
const activityDocMixin = control.hierarchy.classHierarchyMixin(tx.objectClass, activity.mixin.ActivityDoc) const activityDocMixin = control.hierarchy.classHierarchyMixin(tx.objectClass, activity.mixin.ActivityDoc)
if (activityDocMixin === undefined) { if (activityDocMixin === undefined) {
return [] continue
} }
const messages = await control.findAll( const messages = await control.findAll(
@ -449,7 +448,11 @@ async function OnDocRemoved (originTx: TxCUD<Doc>, control: TriggerControl): Pro
{ projection: { _id: 1, _class: 1, space: 1 } } { projection: { _id: 1, _class: 1, space: 1 } }
) )
return messages.map((message) => control.txFactory.createTxRemoveDoc(message._class, message.space, message._id)) result.push(
...messages.map((message) => control.txFactory.createTxRemoveDoc(message._class, message.space, message._id))
)
}
return result
} }
async function ReactionNotificationContentProvider ( async function ReactionNotificationContentProvider (

View File

@ -13,6 +13,15 @@
// limitations under the License. // 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, { import core, {
AccountRole, AccountRole,
AttachedDoc, AttachedDoc,
@ -27,17 +36,8 @@ import core, {
TxUpdateDoc, TxUpdateDoc,
UserStatus UserStatus
} from '@hcengineering/core' } 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 { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector' import { TriggerControl } from '@hcengineering/server-core'
import { createAccountRequest, getSupportWorkspaceId, sendAIEvents } from './utils' import { createAccountRequest, getSupportWorkspaceId, sendAIEvents } from './utils'
@ -241,26 +241,29 @@ async function onSupportWorkspaceMessage (control: TriggerControl, message: Chat
} }
export async function OnMessageSend ( export async function OnMessageSend (
originTx: TxCollectionCUD<Doc, AttachedDoc>, originTxs: TxCollectionCUD<Doc, AttachedDoc>[],
control: TriggerControl control: TriggerControl
): Promise<Tx[]> { ): Promise<Tx[]> {
const { hierarchy } = control const { hierarchy } = control
const tx = TxProcessor.extractTx(originTx) as TxCreateDoc<ChatMessage> const txes = originTxs
if (tx._class !== core.class.TxCreateDoc || !hierarchy.isDerived(tx.objectClass, chunter.class.ChatMessage)) { .map((it) => TxProcessor.extractTx(it) as TxCreateDoc<ChatMessage>)
.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 [] return []
} }
for (const tx of txes) {
if (tx.modifiedBy === aiBot.account.AIBot || tx.modifiedBy === core.account.System) {
return []
}
const isThread = hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage) const isThread = hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage)
const message = TxProcessor.createDoc2Doc(tx) const message = TxProcessor.createDoc2Doc(tx)
const docClass = isThread ? (message as ThreadMessage).objectClass : message.attachedToClass const docClass = isThread ? (message as ThreadMessage).objectClass : message.attachedToClass
if (!hierarchy.isDerived(docClass, chunter.class.ChunterSpace)) { if (!hierarchy.isDerived(docClass, chunter.class.ChunterSpace)) {
return [] continue
} }
if (docClass === chunter.class.DirectMessage) { if (docClass === chunter.class.DirectMessage) {
@ -270,6 +273,7 @@ export async function OnMessageSend (
if (docClass === analyticsCollector.class.OnboardingChannel) { if (docClass === analyticsCollector.class.OnboardingChannel) {
await onSupportWorkspaceMessage(control, message) await onSupportWorkspaceMessage(control, message)
} }
}
return [] return []
} }

View File

@ -35,7 +35,8 @@ import core, {
TxProcessor, TxProcessor,
TxRemoveDoc, TxRemoveDoc,
TxUpdateDoc, TxUpdateDoc,
UserStatus UserStatus,
type MeasureContext
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification' import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification'
import { getMetadata, IntlString, translate } from '@hcengineering/platform' import { getMetadata, IntlString, translate } from '@hcengineering/platform'
@ -151,7 +152,7 @@ async function OnThreadMessageCreated (originTx: TxCUD<Doc>, control: TriggerCon
return [lastReplyTx, repliedPersonTx] return [lastReplyTx, repliedPersonTx]
} }
async function OnChatMessageCreated (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> { async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy const hierarchy = control.hierarchy
const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<ChatMessage> const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<ChatMessage>
@ -163,9 +164,7 @@ async function OnChatMessageCreated (tx: TxCUD<Doc>, control: TriggerControl): P
return [] return []
} }
const targetDoc = ( const targetDoc = (await control.findAll(ctx, message.attachedToClass, { _id: message.attachedTo }, { limit: 1 }))[0]
await control.findAll(control.ctx, message.attachedToClass, { _id: message.attachedTo }, { limit: 1 })
)[0]
if (targetDoc === undefined) { if (targetDoc === undefined) {
return [] return []
} }
@ -190,7 +189,7 @@ async function OnChatMessageCreated (tx: TxCUD<Doc>, control: TriggerControl): P
) )
} }
} else { } else {
const collaborators = await getDocCollaborators(control.ctx, targetDoc, mixin, control) const collaborators = await getDocCollaborators(ctx, targetDoc, mixin, control)
if (!collaborators.includes(message.modifiedBy)) { if (!collaborators.includes(message.modifiedBy)) {
collaborators.push(message.modifiedBy) collaborators.push(message.modifiedBy)
} }
@ -272,33 +271,19 @@ export async function ChunterTrigger (tx: TxCUD<Doc>, control: TriggerControl):
actualTx._class === core.class.TxCreateDoc && actualTx._class === core.class.TxCreateDoc &&
control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage) control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage)
) { ) {
res.push( res.push(...(await control.ctx.with('OnThreadMessageCreated', {}, (ctx) => OnThreadMessageCreated(tx, control))))
...(await control.ctx.with(
'OnThreadMessageCreated',
{},
async (ctx) => await OnThreadMessageCreated(tx, control)
))
)
} }
if ( if (
actualTx._class === core.class.TxRemoveDoc && actualTx._class === core.class.TxRemoveDoc &&
control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage) control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage)
) { ) {
res.push( res.push(...(await control.ctx.with('OnThreadMessageDeleted', {}, (ctx) => OnThreadMessageDeleted(tx, control))))
...(await control.ctx.with(
'OnThreadMessageDeleted',
{},
async (ctx) => await OnThreadMessageDeleted(tx, control)
))
)
} }
if ( if (
actualTx._class === core.class.TxCreateDoc && actualTx._class === core.class.TxCreateDoc &&
control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ChatMessage) control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ChatMessage)
) { ) {
res.push( res.push(...(await control.ctx.with('OnChatMessageCreated', {}, (ctx) => OnChatMessageCreated(ctx, tx, control))))
...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control)))
)
} }
return res return res
} }

View File

@ -13,28 +13,26 @@
// limitations under the License. // limitations under the License.
// //
import { removeCollaborativeDoc } from '@hcengineering/collaboration'
import type { CollaborativeDoc, Doc, Tx, TxRemoveDoc } from '@hcengineering/core' import type { CollaborativeDoc, Doc, Tx, TxRemoveDoc } from '@hcengineering/core'
import core, { TxProcessor } from '@hcengineering/core' import core, { TxProcessor } from '@hcengineering/core'
import { removeCollaborativeDoc } from '@hcengineering/collaboration'
import { type TriggerControl } from '@hcengineering/server-core' import { type TriggerControl } from '@hcengineering/server-core'
/** /**
* @public * @public
*/ */
export async function OnDelete ( export async function OnDelete (
tx: Tx, txes: Tx[],
{ hierarchy, storageAdapter, workspace, removedMap, ctx }: TriggerControl { hierarchy, storageAdapter, workspace, removedMap, ctx }: TriggerControl
): Promise<Tx[]> { ): Promise<Tx[]> {
const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc<Doc> const ltxes = txes
.map((it) => TxProcessor.extractTx(it))
if (rmTx._class !== core.class.TxRemoveDoc) { .filter((it) => it._class === core.class.TxRemoveDoc) as TxRemoveDoc<Doc>[]
return [] for (const rmTx of ltxes) {
}
// Obtain document being deleted // Obtain document being deleted
const doc = removedMap.get(rmTx.objectId) const doc = removedMap.get(rmTx.objectId)
if (doc === undefined) { if (doc === undefined) {
return [] continue
} }
// Ids of files to delete from storage // Ids of files to delete from storage
@ -54,7 +52,7 @@ export async function OnDelete (
// Even though we are deleting it here, the document can be currently in use by someone else // 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 // and when editing session ends, the collborator service will recreate the document again
await removeCollaborativeDoc(storageAdapter, workspace, toDelete, ctx) await removeCollaborativeDoc(storageAdapter, workspace, toDelete, ctx)
}
return [] return []
} }

View File

@ -1001,9 +1001,10 @@ export async function createCollabDocInfo (
} }
const settings = await getNotificationProviderControl(ctx, control) const settings = await getNotificationProviderControl(ctx, control)
const subscriptions = await control.findAll(ctx, notification.class.PushSubscription, { const subscriptions = (await control.queryFind(ctx, notification.class.PushSubscription, {})).filter((it) =>
user: { $in: Array.from(targets) } targets.has(it.user as Ref<PersonAccount>)
}) )
for (const target of targets) { for (const target of targets) {
const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(target)) const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(target))
@ -1938,13 +1939,12 @@ async function OnEmployeeDeactivate (tx: TxCUD<Doc>, control: TriggerControl): P
return res return res
} }
async function OnDocRemove (originTx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> { async function OnDocRemove (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
const tx = TxProcessor.extractTx(originTx) as TxRemoveDoc<Doc> const ltxes = txes
.map((it) => TxProcessor.extractTx(it))
if (tx._class !== core.class.TxRemoveDoc) return [] .filter((it) => it._class === core.class.TxRemoveDoc) as TxRemoveDoc<Doc>[]
const res: Tx[] = [] const res: Tx[] = []
for (const tx of ltxes) {
if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) { if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) {
const message = control.removedMap.get(tx.objectId) as ActivityMessage | undefined const message = control.removedMap.get(tx.objectId) as ActivityMessage | undefined
@ -1962,12 +1962,11 @@ async function OnDocRemove (originTx: TxCUD<Doc>, control: TriggerControl): Prom
} }
} }
return await removeContextNotifications(control, [tx.objectId as Ref<DocNotifyContext>]) res.push(...(await removeContextNotifications(control, [tx.objectId as Ref<DocNotifyContext>])))
} }
const txes = await removeCollaboratorDoc(tx, control) res.push(...(await removeCollaboratorDoc(tx, control)))
}
res.push(...txes)
return res return res
} }

View File

@ -13,55 +13,51 @@
// limitations under the License. // limitations under the License.
// //
import { DocUpdateMessage } from '@hcengineering/activity'
import { PersonAccount } from '@hcengineering/contact'
import core, { import core, {
Doc, Doc,
Ref,
Tx, Tx,
TxCUD, TxCUD,
TxCollectionCUD, TxCollectionCUD,
TxCreateDoc, TxCreateDoc,
TxUpdateDoc,
TxProcessor, TxProcessor,
Ref, TxUpdateDoc,
type MeasureContext type MeasureContext
} from '@hcengineering/core' } 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 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 { import {
getNotificationTxes,
getCollaborators, getCollaborators,
getNotificationProviderControl,
getNotificationTxes,
getTextPresenter, getTextPresenter,
getUsersInfo, getUsersInfo,
toReceiverInfo, toReceiverInfo
getNotificationProviderControl
} from '@hcengineering/server-notification-resources' } from '@hcengineering/server-notification-resources'
import { PersonAccount } from '@hcengineering/contact'
/** /**
* @public * @public
*/ */
export async function OnRequest (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnRequest (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
if (tx._class !== core.class.TxCollectionCUD) {
return []
}
const hierarchy = control.hierarchy const hierarchy = control.hierarchy
const ptx = tx as TxCollectionCUD<Doc, Request> const ltxes = (
txes.filter((it) => it._class === core.class.TxCollectionCUD) as TxCollectionCUD<Doc, Request>[]
if (!hierarchy.isDerived(ptx.tx.objectClass, request.class.Request)) { ).filter((it) => hierarchy.isDerived(it.tx.objectClass, request.class.Request))
return []
}
let res: Tx[] = [] let res: Tx[] = []
for (const ptx of ltxes) {
res = res.concat(await getRequestNotificationTx(control.ctx, ptx, control)) res = res.concat(await getRequestNotificationTx(control.ctx, ptx, control))
if (ptx.tx._class === core.class.TxUpdateDoc) { if (ptx.tx._class === core.class.TxUpdateDoc) {
res = res.concat(await OnRequestUpdate(ptx, control)) res = res.concat(await OnRequestUpdate(ptx, control))
} }
}
return res return res
} }
@ -168,9 +164,10 @@ async function getRequestNotificationTx (
} }
const notificationControl = await getNotificationProviderControl(ctx, control) const notificationControl = await getNotificationProviderControl(ctx, control)
const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { const collaboratorsSet = new Set(collaborators)
user: { $in: collaborators } const subscriptions = (await control.queryFind(control.ctx, notification.class.PushSubscription, {})).filter((it) =>
}) collaboratorsSet.has(it.user)
)
for (const target of collaborators) { for (const target of collaborators) {
const targetInfo = toReceiverInfo(control.hierarchy, usersInfo.get(target)) const targetInfo = toReceiverInfo(control.hierarchy, usersInfo.get(target))
if (targetInfo === undefined) continue if (targetInfo === undefined) continue

View File

@ -49,7 +49,9 @@ export async function TagElementRemove (
/** /**
* @public * @public
*/ */
export async function onTagReference (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function onTagReference (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) const actualTx = TxProcessor.extractTx(tx)
const isCreate = control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc) const isCreate = control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)
const isRemove = control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc) const isRemove = control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)
@ -57,24 +59,27 @@ export async function onTagReference (tx: Tx, control: TriggerControl): Promise<
if (!control.hierarchy.isDerived((actualTx as TxCUD<Doc>).objectClass, tags.class.TagReference)) return [] if (!control.hierarchy.isDerived((actualTx as TxCUD<Doc>).objectClass, tags.class.TagReference)) return []
if (isCreate) { if (isCreate) {
const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<TagReference>) const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<TagReference>)
const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, { result.push(
control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, {
$inc: { refCount: 1 } $inc: { refCount: 1 }
}) })
return [res] )
} }
if (isRemove) { if (isRemove) {
const ctx = actualTx as TxRemoveDoc<TagReference> const ctx = actualTx as TxRemoveDoc<TagReference>
const doc = control.removedMap.get(ctx.objectId) as TagReference const doc = control.removedMap.get(ctx.objectId) as TagReference
if (doc !== undefined) { if (doc !== undefined) {
if (!control.removedMap.has(doc.tag)) { if (!control.removedMap.has(doc.tag)) {
const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, { result.push(
control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, {
$inc: { refCount: -1 } $inc: { refCount: -1 }
}) })
return [res] )
} }
} }
} }
return [] }
return result
} }
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type

View File

@ -20,31 +20,34 @@ import task, { Task } from '@hcengineering/task'
/** /**
* @public * @public
*/ */
export async function OnStateUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnStateUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc> const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
if (!control.hierarchy.isDerived(actualTx.objectClass, task.class.Task)) return [] if (!control.hierarchy.isDerived(actualTx.objectClass, task.class.Task)) return []
if (actualTx._class === core.class.TxCreateDoc) { if (actualTx._class === core.class.TxCreateDoc) {
const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<Task>) const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<Task>)
const status = (await control.modelDb.findAll(core.class.Status, { _id: doc.status }))[0] const status = control.modelDb.findAllSync(core.class.Status, { _id: doc.status })[0]
if (status?.category === task.statusCategory.Lost || status?.category === task.statusCategory.Won) { if (status?.category === task.statusCategory.Lost || status?.category === task.statusCategory.Won) {
return [control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { isDone: true })] result.push(control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { isDone: true }))
} }
} else if (actualTx._class === core.class.TxUpdateDoc) { } else if (actualTx._class === core.class.TxUpdateDoc) {
const updateTx = actualTx as TxUpdateDoc<Task> const updateTx = actualTx as TxUpdateDoc<Task>
if (updateTx.operations.status !== undefined) { if (updateTx.operations.status !== undefined) {
const status = (await control.modelDb.findAll(core.class.Status, { _id: updateTx.operations.status }))[0] const status = control.modelDb.findAllSync(core.class.Status, { _id: updateTx.operations.status })[0]
if (status?.category === task.statusCategory.Lost || status?.category === task.statusCategory.Won) { if (status?.category === task.statusCategory.Lost || status?.category === task.statusCategory.Won) {
return [ result.push(
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, { control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
isDone: true isDone: true
}) })
] )
} else { } else {
return [ result.push(
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, { control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
isDone: false isDone: false
}) })
] )
}
} }
} }
} }

View File

@ -13,6 +13,8 @@
// limitations under the License. // 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 contact, { Channel, ChannelProvider, Contact, Employee, formatName, PersonAccount } from '@hcengineering/contact'
import { import {
Account, Account,
@ -29,27 +31,25 @@ import {
TxCreateDoc, TxCreateDoc,
TxProcessor TxProcessor
} from '@hcengineering/core' } from '@hcengineering/core'
import { TriggerControl } from '@hcengineering/server-core'
import notification, { import notification, {
BaseNotificationType, BaseNotificationType,
InboxNotification, InboxNotification,
MentionInboxNotification, MentionInboxNotification,
NotificationType NotificationType
} from '@hcengineering/notification' } 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 { 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 { import {
getTranslatedNotificationContent, getNotificationLink,
getTextPresenter, getTextPresenter,
getNotificationLink getTranslatedNotificationContent
} from '@hcengineering/server-notification-resources' } from '@hcengineering/server-notification-resources'
import serverTelegram from '@hcengineering/server-telegram'
import { generateToken } from '@hcengineering/server-token' 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 { markupToHTML } from '@hcengineering/text'
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
/** /**
* @public * @public
@ -245,6 +245,7 @@ function hasAttachments (doc: ActivityMessage | undefined, hierarchy: Hierarchy)
return false return false
} }
const telegramNotificationKey = 'telegram.notification.reported'
const SendTelegramNotifications: NotificationProviderFunc = async ( const SendTelegramNotifications: NotificationProviderFunc = async (
control: TriggerControl, control: TriggerControl,
types: BaseNotificationType[], types: BaseNotificationType[],
@ -261,7 +262,11 @@ const SendTelegramNotifications: NotificationProviderFunc = async (
const botUrl = getMetadata(serverTelegram.metadata.BotUrl) const botUrl = getMetadata(serverTelegram.metadata.BotUrl)
if (botUrl === undefined || botUrl === '') { if (botUrl === undefined || botUrl === '') {
const reported = control.cache.get(telegramNotificationKey)
if (reported === undefined) {
control.ctx.error('Please provide telegram bot service url to enable telegram notifications.') control.ctx.error('Please provide telegram bot service url to enable telegram notifications.')
control.cache.set(telegramNotificationKey, true)
}
return [] return []
} }

View File

@ -50,7 +50,9 @@ import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengine
/** /**
* @public * @public
*/ */
export async function OnTask (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnTask (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc> const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
const mixin = control.hierarchy.classHierarchyMixin<Class<Doc>, ToDoFactory>( const mixin = control.hierarchy.classHierarchyMixin<Class<Doc>, ToDoFactory>(
actualTx.objectClass, actualTx.objectClass,
@ -59,14 +61,15 @@ export async function OnTask (tx: Tx, control: TriggerControl): Promise<Tx[]> {
if (mixin !== undefined) { if (mixin !== undefined) {
if (actualTx._class !== core.class.TxRemoveDoc) { if (actualTx._class !== core.class.TxRemoveDoc) {
const factory = await getResource(mixin.factory) const factory = await getResource(mixin.factory)
return await factory(tx, control) result.push(...(await factory(tx, control)))
} else { } else {
const todos = await control.findAll(control.ctx, time.class.ToDo, { attachedTo: actualTx.objectId }) 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)) 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<Tx[]> { export async function OnWorkSlotUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {

View File

@ -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 { public getAdapter (domain: Domain, requireExists: boolean): DbAdapter {
const name = this.conf.domains[domain] ?? '#default' const name = this.conf.domains[domain] ?? '#default'
const adapter = this.adapters.get(name) ?? this.defaultAdapter const adapter = this.adapters.get(name) ?? this.defaultAdapter

View File

@ -126,6 +126,7 @@ export function initStatisticsContext (
workspaces: ops?.getUsers?.() workspaces: ops?.getUsers?.()
} }
try {
void fetch( void fetch(
concatLink(statsUrl, '/api/v1/statistics') + `/?token=${encodeURIComponent(token)}&name=${serviceId}`, concatLink(statsUrl, '/api/v1/statistics') + `/?token=${encodeURIComponent(token)}&name=${serviceId}`,
{ {
@ -141,6 +142,12 @@ export function initStatisticsContext (
console.error(err) console.error(err)
} }
}) })
} catch (err: any) {
errorToSend++
if (errorToSend % 20 === 0) {
console.error(err)
}
}
} }
}, METRICS_UPDATE_INTERVAL) }, METRICS_UPDATE_INTERVAL)

View File

@ -17,7 +17,6 @@
import core, { import core, {
TxFactory, TxFactory,
TxProcessor, TxProcessor,
generateId,
groupByArray, groupByArray,
matchQuery, matchQuery,
type Class, type Class,
@ -134,7 +133,6 @@ export class Triggers {
mode: 'sync' | 'async' mode: 'sync' | 'async'
): Promise<Tx[]> { ): Promise<Tx[]> {
const result: Tx[] = [] const result: Tx[] = []
for (const { query, trigger, arrays } of this.triggers) { for (const { query, trigger, arrays } of this.triggers) {
if ((trigger.isAsync ? 'async' : 'sync') !== mode) { if ((trigger.isAsync ? 'async' : 'sync') !== mode) {
continue continue
@ -150,19 +148,14 @@ export class Triggers {
trigger.resource, trigger.resource,
{}, {},
async (ctx) => { async (ctx) => {
if (mode === 'async') {
ctx.id = generateId()
}
const tresult = await this.applyTrigger(ctx, ctrl, matches, { trigger, arrays }) const tresult = await this.applyTrigger(ctx, ctrl, matches, { trigger, arrays })
result.push(...tresult) result.push(...tresult)
if (ctx.onEnd !== undefined && mode === 'async') {
await ctx.onEnd(ctx)
}
}, },
{ count: matches.length, arrays } { count: matches.length, arrays }
) )
} }
} }
return result return result
} }

View File

@ -141,6 +141,9 @@ export type TxMiddlewareResult = TxResult
export interface DBAdapterManager { export interface DBAdapterManager {
getAdapter: (domain: Domain, requireExists: boolean) => DbAdapter getAdapter: (domain: Domain, requireExists: boolean) => DbAdapter
getAdapterName: (domain: Domain) => string
getAdapterByName: (name: string, requireExists: boolean) => DbAdapter
getDefaultAdapter: () => DbAdapter getDefaultAdapter: () => DbAdapter
close: () => Promise<void> close: () => Promise<void>

View File

@ -18,7 +18,6 @@ import type { ContentTextAdapterConfiguration, FullTextAdapterFactory } from '@h
export * from './fulltext' export * from './fulltext'
export * from './indexer' export * from './indexer'
export * from './rekoni' export * from './rekoni'
export * from './ydoc'
export interface FulltextDBConfiguration { export interface FulltextDBConfiguration {
fulltextAdapter: { fulltextAdapter: {

View File

@ -63,7 +63,7 @@ import type {
StorageAdapter StorageAdapter
} from '@hcengineering/server-core' } from '@hcengineering/server-core'
import { RateLimiter, SessionDataImpl } 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 { findSearchPresenter, updateDocWithPresenter } from '../mapper'
import { type FullTextPipeline } from './types' import { type FullTextPipeline } from './types'
import { createIndexedDoc, createStateDoc, getContent } from './utils' import { createIndexedDoc, createStateDoc, getContent } from './utils'
@ -786,12 +786,21 @@ export class FullTextIndexPipeline implements FullTextPipeline {
if (collaborativeDoc !== undefined && collaborativeDoc !== '') { if (collaborativeDoc !== undefined && collaborativeDoc !== '') {
const { documentId } = collaborativeDocParse(collaborativeDoc) const { documentId } = collaborativeDocParse(collaborativeDoc)
const docInfo: Blob | undefined = await this.storageAdapter?.stat(ctx, this.workspace, documentId)
try { 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) { } catch (err: any) {
Analytics.handleError(err) 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 })
} }
} }
} }

View File

@ -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<ContentTextAdapter> {
return {
content: async (
ctx: MeasureContext,
workspace: WorkspaceId,
_name: string,
_type: string,
data: Readable | Buffer | string
): Promise<string> => {
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 ''
}
}
}

View File

@ -14,9 +14,10 @@
// //
import core, { import core, {
Domain,
groupByArray,
TxProcessor, TxProcessor,
type Doc, type Doc,
type Domain,
type MeasureContext, type MeasureContext,
type SessionData, type SessionData,
type Tx, type Tx,
@ -24,7 +25,13 @@ import core, {
type TxResult type TxResult
} from '@hcengineering/core' } from '@hcengineering/core'
import { PlatformError, unknownError } from '@hcengineering/platform' 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' import { BaseMiddleware } from '@hcengineering/server-core'
/** /**
@ -66,32 +73,41 @@ export class DomainTxMiddleware extends BaseMiddleware implements Middleware {
private async routeTx (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<TxResult[]> { private async routeTx (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<TxResult[]> {
const result: TxResult[] = [] const result: TxResult[] = []
const domainGroups = new Map<Domain, TxCUD<Doc>[]>() const adapterGroups = new Map<string, TxCUD<Doc>[]>()
const routeToAdapter = async (domain: Domain, txes: TxCUD<Doc>[]): Promise<void> => { const routeToAdapter = async (adapter: DbAdapter, txes: TxCUD<Doc>[]): Promise<void> => {
if (txes.length > 0) { if (txes.length > 0) {
// Find all deleted documents // Find all deleted documents
const toDelete = txes.filter((it) => it._class === core.class.TxRemoveDoc)
const adapter = this.adapterManager.getAdapter(domain, true)
const toDelete = txes.filter((it) => it._class === core.class.TxRemoveDoc).map((it) => it.objectId)
if (toDelete.length > 0) { if (toDelete.length > 0) {
const toDeleteDocs = await ctx.with( const deleteByDomain = groupByArray(toDelete, (it) => this.context.hierarchy.getDomain(it.objectClass))
for (const [d, docs] of deleteByDomain.entries()) {
const todel = await ctx.with(
'adapter-load', 'adapter-load',
{ domain }, {},
async () => await adapter.load(ctx, domain, toDelete), () =>
adapter.load(
ctx,
d,
docs.map((it) => it._id)
),
{ count: toDelete.length } { count: toDelete.length }
) )
for (const ddoc of toDeleteDocs) { for (const ddoc of todel) {
ctx.contextData.removedMap.set(ddoc._id, ddoc) 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, txes: txes.length,
classes: Array.from(new Set(txes.map((it) => it.objectClass))), classes,
_classes: Array.from(new Set(txes.map((it) => it._class))) _classes
}) })
if (Array.isArray(r)) { if (Array.isArray(r)) {
@ -102,6 +118,7 @@ export class DomainTxMiddleware extends BaseMiddleware implements Middleware {
} }
} }
const domains = new Set<Domain>()
for (const tx of txes) { for (const tx of txes) {
const txCUD = TxProcessor.extractTx(tx) as TxCUD<Doc> const txCUD = TxProcessor.extractTx(tx) as TxCUD<Doc>
if (!TxProcessor.isExtendsCUD(txCUD._class)) { if (!TxProcessor.isExtendsCUD(txCUD._class)) {
@ -110,16 +127,26 @@ export class DomainTxMiddleware extends BaseMiddleware implements Middleware {
continue continue
} }
const domain = this.context.hierarchy.getDomain(txCUD.objectClass) 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) { if (group === undefined) {
group = [] group = []
domainGroups.set(domain, group) adapterGroups.set(adapterName, group)
} }
group.push(txCUD) 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 return result
} }

View File

@ -89,9 +89,7 @@ export class LiveQueryMiddleware extends BaseMiddleware implements Middleware {
} }
async tx (ctx: MeasureContext, tx: Tx[]): Promise<TxMiddlewareResult> { async tx (ctx: MeasureContext, tx: Tx[]): Promise<TxMiddlewareResult> {
for (const _tx of tx) { await this.liveQuery.tx(...tx)
await this.liveQuery.tx(_tx)
}
return await this.provideTx(ctx, tx) return await this.provideTx(ctx, tx)
} }
} }

View File

@ -54,8 +54,9 @@ export class QueryJoiner {
} }
if (q.result instanceof Promise) { if (q.result instanceof Promise) {
q.result = await q.result q.result = await q.result
q.callbacks--
} }
q.callbacks--
this.removeFromQueue(q) this.removeFromQueue(q)
return q.result as FindResult<T> return q.result as FindResult<T>

View File

@ -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<void>((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)
})
})

View File

@ -51,6 +51,7 @@ import type {
} from '@hcengineering/server-core' } from '@hcengineering/server-core'
import serverCore, { BaseMiddleware, SessionDataImpl, SessionFindAll, Triggers } from '@hcengineering/server-core' import serverCore, { BaseMiddleware, SessionDataImpl, SessionFindAll, Triggers } from '@hcengineering/server-core'
import { filterBroadcastOnly } from './utils' import { filterBroadcastOnly } from './utils'
import { QueryJoiner } from './queryJoin'
/** /**
* @public * @public
@ -106,13 +107,14 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
} }
private async processDerived (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<void> { private async processDerived (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<void> {
const findAll: SessionFindAll = async (ctx, _class, query, options) => { const _findAll: SessionFindAll = async (ctx, _class, query, options) => {
const _ctx: MeasureContext = (options as ServerFindOptions<Doc>)?.ctx ?? ctx const _ctx: MeasureContext = (options as ServerFindOptions<Doc>)?.ctx ?? ctx
delete (options as ServerFindOptions<Doc>)?.ctx delete (options as ServerFindOptions<Doc>)?.ctx
if (_ctx.contextData !== undefined) { if (_ctx.contextData !== undefined) {
_ctx.contextData.isTriggerCtx = true _ctx.contextData.isTriggerCtx = true
} }
// Use live query
const results = await this.findAll(_ctx, _class, query, options) const results = await this.findAll(_ctx, _class, query, options)
return toFindResult( return toFindResult(
results.map((v) => { results.map((v) => {
@ -121,6 +123,12 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
results.total 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 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 collections = await ctx.with('process-collection', {}, (ctx) => this.processCollection(ctx, txes, findAll))
const moves = await ctx.with('process-move', {}, (ctx) => this.processMove(ctx, txes, findAll)) const 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 this.context.modelDb
) )
ctx.contextData = asyncContextData ctx.contextData = asyncContextData
if (!((ctx as MeasureContext<SessionDataImpl>).contextData.isAsyncContext ?? false)) {
ctx.id = generateId()
}
const aresult = await this.triggers.apply( const aresult = await this.triggers.apply(
ctx, ctx,
txes, txes,
@ -236,16 +248,15 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
) )
if (aresult.length > 0) { if (aresult.length > 0) {
await ctx.with('process-aync-result', {}, async (ctx) => { await ctx.with('process-async-result', {}, async (ctx) => {
ctx.id = generateId()
await this.processDerivedTxes(ctx, aresult) await this.processDerivedTxes(ctx, aresult)
// We need to send all to recipients // We need to send all to recipients
await this.context.head?.handleBroadcast(ctx) await this.context.head?.handleBroadcast(ctx)
})
}
if (ctx.onEnd !== undefined) { if (ctx.onEnd !== undefined) {
await ctx.onEnd(ctx) await ctx.onEnd(ctx)
} }
})
}
} }
private async processDerivedTxes (ctx: MeasureContext<SessionData>, derived: Tx[]): Promise<void> { private async processDerivedTxes (ctx: MeasureContext<SessionData>, derived: Tx[]): Promise<void> {

View File

@ -14,6 +14,7 @@ export { serverGithubId } from '@hcengineering/server-github'
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverGithub.trigger.OnProjectChanges, trigger: serverGithub.trigger.OnProjectChanges,
arrays: true,
isAsync: true isAsync: true
}) })

View File

@ -29,17 +29,22 @@ import tracker from '@hcengineering/tracker'
/** /**
* @public * @public
*/ */
export async function OnProjectChanges (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnProjectChanges (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
const cache = new Map<string, any>()
const toApply: Tx[] = []
for (const tx of txes) {
const ltx = TxProcessor.extractTx(tx) const ltx = TxProcessor.extractTx(tx)
if (ltx._class === core.class.TxMixin && (ltx as TxMixin<Doc, Doc>).mixin === github.mixin.GithubIssue) { if (ltx._class === core.class.TxMixin && (ltx as TxMixin<Doc, Doc>).mixin === github.mixin.GithubIssue) {
const mix = ltx as TxMixin<Doc, Doc> const mix = ltx as TxMixin<Doc, Doc>
// Do not spend time to wait for trigger processing // Do not spend time to wait for trigger processing
await updateDocSyncInfo(control, tx, mix.objectSpace, mix) await updateDocSyncInfo(control, tx, mix.objectSpace, mix, cache, toApply)
return [] continue
} }
if (control.hierarchy.isDerived(ltx._class, core.class.TxCUD)) { if (TxProcessor.isExtendsCUD(ltx._class)) {
const cud = ltx as TxCUD<Doc> const cud = ltx as TxCUD<Doc>
let space: Ref<Space> = cud.objectSpace let space: Ref<Space> = cud.objectSpace
@ -52,15 +57,14 @@ export async function OnProjectChanges (tx: Tx, control: TriggerControl): Promis
} }
if (isDocSyncUpdateRequired(control.hierarchy, cud)) { if (isDocSyncUpdateRequired(control.hierarchy, cud)) {
// Do not spend time to wait for trigger processing await updateDocSyncInfo(control, tx, space, cud, cache, toApply)
await updateDocSyncInfo(control, tx, space, cud)
} }
if (control.hierarchy.isDerived(cud.objectClass, time.class.ToDo)) { if (control.hierarchy.isDerived(cud.objectClass, time.class.ToDo)) {
if (tx._class === core.class.TxCollectionCUD) { if (tx._class === core.class.TxCollectionCUD) {
const coll = tx as TxCollectionCUD<Doc, AttachedDoc> const coll = tx as TxCollectionCUD<Doc, AttachedDoc>
if (control.hierarchy.isDerived(coll.objectClass, github.class.GithubPullRequest)) { if (control.hierarchy.isDerived(coll.objectClass, github.class.GithubPullRequest)) {
// Ok we got todo change for pull request, let's mark it for sync. // Ok we got todo change for pull request, let's mark it for sync.
return [ result.push(
control.txFactory.createTxUpdateDoc<DocSyncInfo>( control.txFactory.createTxUpdateDoc<DocSyncInfo>(
github.class.DocSyncInfo, github.class.DocSyncInfo,
coll.objectSpace, coll.objectSpace,
@ -69,12 +73,16 @@ export async function OnProjectChanges (tx: Tx, control: TriggerControl): Promis
needSync: '' needSync: ''
} }
) )
] )
} }
} }
} }
} }
return [] }
if (toApply.length > 0) {
await control.apply(control.ctx, toApply)
}
return result
} }
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@ -108,14 +116,16 @@ async function updateDocSyncInfo (
_class: Ref<Class<Tx>> _class: Ref<Class<Tx>>
objectId: Ref<Doc> objectId: Ref<Doc>
objectClass: Ref<Class<Doc>> objectClass: Ref<Class<Doc>>
} },
cache: Map<string, any>,
toApply: Tx[]
): Promise<void> { ): Promise<void> {
const checkTx = (tx: Tx): boolean => const checkTx = (tx: Tx): boolean =>
control.hierarchy.isDerived(tx._class, core.class.TxCUD) && control.hierarchy.isDerived(tx._class, core.class.TxCUD) &&
(tx as TxCUD<Doc>).objectClass === github.class.DocSyncInfo && (tx as TxCUD<Doc>).objectClass === github.class.DocSyncInfo &&
(tx as TxCUD<Doc>).objectId === cud.objectId (tx as TxCUD<Doc>).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 // Check already captured Txes
for (const i of txes) { for (const i of txes) {
if (checkTx(i)) { if (checkTx(i)) {
@ -131,20 +141,29 @@ async function updateDocSyncInfo (
if (account === undefined) { if (account === undefined) {
return 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<GithubProject>))) { if (projects.some((it) => it._id === (space as Ref<GithubProject>))) {
const [sdoc] = await control.findAll(control.ctx, github.class.DocSyncInfo, { const sdoc =
(cache.get(cud.objectId) as DocSyncInfo) ??
(
await control.findAll(control.ctx, github.class.DocSyncInfo, {
_id: cud.objectId as Ref<DocSyncInfo> _id: cud.objectId as Ref<DocSyncInfo>
}) })
).shift()
// We need to check if sync doc is already exists. // We need to check if sync doc is already exists.
if (sdoc === undefined) { if (sdoc === undefined) {
// Created by non github integration // Created by non github integration
// We need to create the doc sync info // We need to create the doc sync info
await createSyncDoc(control, cud, tx, space) createSyncDoc(control, cud, tx, space, toApply)
} else { } else {
cache.set(cud.objectId, sdoc)
// We need to create the doc sync info // 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<Doc>): boolean {
) )
} }
async function updateSyncDoc ( function updateSyncDoc (
control: TriggerControl, control: TriggerControl,
cud: { cud: {
_class: Ref<Class<Tx>> _class: Ref<Class<Tx>>
@ -168,8 +187,9 @@ async function updateSyncDoc (
objectClass: Ref<Class<Doc>> objectClass: Ref<Class<Doc>>
}, },
space: Ref<Space>, space: Ref<Space>,
info: DocSyncInfo info: DocSyncInfo,
): Promise<void> { toApply: Tx[]
): void {
const data: DocumentUpdate<DocSyncInfo> = const data: DocumentUpdate<DocSyncInfo> =
cud._class === core.class.TxRemoveDoc 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.externalVersion = '#' // We need to put this one to handle new documents.)
data.space = space data.space = space
} }
await control.apply(control.ctx, [ toApply.push(
control.txFactory.createTxUpdateDoc<DocSyncInfo>( control.txFactory.createTxUpdateDoc<DocSyncInfo>(
github.class.DocSyncInfo, github.class.DocSyncInfo,
info.space, info.space,
cud.objectId as Ref<DocSyncInfo>, cud.objectId as Ref<DocSyncInfo>,
data data
) )
]) )
control.ctx.contextData.broadcast.targets.github = (it) => { control.ctx.contextData.broadcast.targets.github = (it) => {
if (control.hierarchy.isDerived(it._class, core.class.TxCUD)) { if (control.hierarchy.isDerived(it._class, core.class.TxCUD)) {
@ -201,7 +221,7 @@ async function updateSyncDoc (
} }
} }
async function createSyncDoc ( function createSyncDoc (
control: TriggerControl, control: TriggerControl,
cud: { cud: {
_class: Ref<Class<Tx>> _class: Ref<Class<Tx>>
@ -209,8 +229,9 @@ async function createSyncDoc (
objectClass: Ref<Class<Doc>> objectClass: Ref<Class<Doc>>
}, },
tx: Tx, tx: Tx,
space: Ref<Space> space: Ref<Space>,
): Promise<void> { toApply: Tx[]
): void {
const data: DocumentUpdate<DocSyncInfo> = { const data: DocumentUpdate<DocSyncInfo> = {
url: '', url: '',
githubNumber: 0, githubNumber: 0,
@ -226,14 +247,14 @@ async function createSyncDoc (
data.attachedTo = coll.objectId data.attachedTo = coll.objectId
} }
await control.apply(control.ctx, [ toApply.push(
control.txFactory.createTxCreateDoc<DocSyncInfo>( control.txFactory.createTxCreateDoc<DocSyncInfo>(
github.class.DocSyncInfo, github.class.DocSyncInfo,
space, space,
data, data,
cud.objectId as Ref<DocSyncInfo> cud.objectId as Ref<DocSyncInfo>
) )
]) )
control.ctx.contextData.broadcast.targets.github = (it) => { control.ctx.contextData.broadcast.targets.github = (it) => {
if (control.hierarchy.isDerived(it._class, core.class.TxCUD)) { if (control.hierarchy.isDerived(it._class, core.class.TxCUD)) {
if ((it as TxCUD<Doc>).objectClass === github.class.DocSyncInfo) { if ((it as TxCUD<Doc>).objectClass === github.class.DocSyncInfo) {

View File

@ -56,6 +56,7 @@ services:
- STORAGE_CONFIG=${STORAGE_CONFIG} - STORAGE_CONFIG=${STORAGE_CONFIG}
- MODEL_ENABLED=* - MODEL_ENABLED=*
- BRANDING_PATH=/var/cfg/branding.json - BRANDING_PATH=/var/cfg/branding.json
- STATS_URL=http://stats:4901
workspace: workspace:
image: hardcoreeng/workspace image: hardcoreeng/workspace
links: links:
@ -72,6 +73,7 @@ services:
- MODEL_ENABLED=* - MODEL_ENABLED=*
- ACCOUNTS_URL=http://account:3003 - ACCOUNTS_URL=http://account:3003
- BRANDING_PATH=/var/cfg/branding.json - BRANDING_PATH=/var/cfg/branding.json
- STATS_URL=http://stats:4901
restart: unless-stopped restart: unless-stopped
front: front:
image: hardcoreeng/front image: hardcoreeng/front
@ -100,6 +102,8 @@ services:
- COLLABORATOR_URL=ws://localhost:3079 - COLLABORATOR_URL=ws://localhost:3079
- STORAGE_CONFIG=${STORAGE_CONFIG} - STORAGE_CONFIG=${STORAGE_CONFIG}
- BRANDING_URL=http://localhost:8083/branding-test.json - BRANDING_URL=http://localhost:8083/branding-test.json
- STATS_URL=http://stats:4901
- STATS_API=http://localhost:4901
transactor: transactor:
image: hardcoreeng/transactor image: hardcoreeng/transactor
pull_policy: never pull_policy: never
@ -128,6 +132,7 @@ services:
- LAST_NAME_FIRST=true - LAST_NAME_FIRST=true
- BRANDING_PATH=/var/cfg/branding.json - BRANDING_PATH=/var/cfg/branding.json
- FULLTEXT_URL=http://fulltext:4710 - FULLTEXT_URL=http://fulltext:4710
- STATS_URL=http://stats:4901
collaborator: collaborator:
image: hardcoreeng/collaborator image: hardcoreeng/collaborator
links: links:
@ -142,20 +147,18 @@ services:
- ACCOUNTS_URL=http://account:3003 - ACCOUNTS_URL=http://account:3003
- STORAGE_CONFIG=${STORAGE_CONFIG} - STORAGE_CONFIG=${STORAGE_CONFIG}
- FULLTEXT_URL=http://fulltext:4710 - FULLTEXT_URL=http://fulltext:4710
- STATS_URL=http://stats:4901
restart: unless-stopped restart: unless-stopped
rekoni: rekoni:
image: hardcoreeng/rekoni-service image: hardcoreeng/rekoni-service
restart: on-failure restart: on-failure
ports: ports:
- 4007:4004 - 4007:4004
deploy: environment:
resources: - STATS_URL=http://stats:4901
limits:
memory: 1024M
fulltext: fulltext:
image: hardcoreeng/fulltext image: hardcoreeng/fulltext
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped restart: unless-stopped
links: links:
- elastic - elastic
@ -173,3 +176,12 @@ services:
- STORAGE_CONFIG=${STORAGE_CONFIG} - STORAGE_CONFIG=${STORAGE_CONFIG}
- REKONI_URL=http://rekoni:4007 - REKONI_URL=http://rekoni:4007
- ACCOUNTS_URL=http://account:3003 - 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

View File

@ -23,6 +23,8 @@ fi
# Create user record in accounts # Create user record in accounts
./tool.sh create-account user1 -f John -l Appleseed -p 1234 ./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 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 user1 sanity-ws
./tool.sh assign-workspace user2 sanity-ws ./tool.sh assign-workspace user2 sanity-ws
./tool.sh set-user-role user1 sanity-ws OWNER ./tool.sh set-user-role user1 sanity-ws OWNER

View File

@ -253,29 +253,40 @@ test.describe('Inbox tests', () => {
const linkText = await page.locator('.antiPopup .link').textContent() const linkText = await page.locator('.antiPopup .link').textContent()
const page2 = await browser.newPage() const page2 = await browser.newPage()
try { try {
const leftSideMenuPageSecond = new LeftSideMenuPage(page2) const channelPage2 = new ChannelPage(page2)
const inboxPageSecond = new InboxPage(page2) const leftSideMenuPage2 = new LeftSideMenuPage(page2)
const inboxPage2 = new InboxPage(page2)
await leftSideMenuPage.clickOnCloseInvite() await leftSideMenuPage.clickOnCloseInvite()
await page2.goto(linkText ?? '') 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 leftSideMenuPage.clickChunter()
await channelPage.clickChannel('general') await channelPage.clickChannel('general')
await channelPage.sendMessage('Test message') await channelPage.sendMessage('Test message')
await leftSideMenuPage2.clickNotification()
await inboxPage2.clickOnInboxFilter('Channels')
await leftSideMenuPage.clickTracker() await leftSideMenuPage.clickTracker()
const newIssue = createNewIssueData(newUser2.firstName, newUser2.lastName) const newIssue = createNewIssueData(newUser2.firstName, newUser2.lastName)
await prepareNewIssueWithOpenStep(page, newIssue, false) await prepareNewIssueWithOpenStep(page, newIssue, false)
await issuesDetailsPage.checkIssue(newIssue) await issuesDetailsPage.checkIssue(newIssue)
await leftSideMenuPageSecond.clickTracker()
await leftSideMenuPageSecond.clickNotification() await leftSideMenuPage2.clickTracker()
await inboxPageSecond.clickOnInboxFilter('Channels') await leftSideMenuPage2.clickNotification()
await inboxPageSecond.checkIfInboxChatExists(newIssue.title, false)
await inboxPageSecond.checkIfInboxChatExists('Test message', true) await inboxPage2.clickOnInboxFilter('Channels')
await inboxPageSecond.clickOnInboxFilter('Issues') await inboxPage2.checkIfInboxChatExists(newIssue.title, false)
await inboxPageSecond.checkIfIssueIsPresentInInbox(newIssue.title) await inboxPage2.checkIfInboxChatExists('Test message', true)
await inboxPageSecond.checkIfInboxChatExists('Channel general', false) await inboxPage2.clickOnInboxFilter('Issues')
await inboxPage2.checkIfIssueIsPresentInInbox(newIssue.title)
await inboxPage2.checkIfInboxChatExists('Channel general', false)
} finally { } finally {
await page2.close() await page2.close()
} }