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",
"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",

View File

@ -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:

View File

@ -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<void> {
{ 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<void> {
documents.class.DocumentCategory,
documents.space.QualityDocuments,
{ ...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()

View File

@ -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<Doc>(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<TagCategory>
(recruit.category.Category + '.' + c.id) as Ref<TagCategory>,
existingCategories
)
}
await ops.commit()
await createSequence(tx, recruit.class.Review)
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, {
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, {

View File

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

View File

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

View File

@ -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, {

View File

@ -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, {

View File

@ -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<Class<Doc>, ObjectDDParticipant>(

View File

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

View File

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

View File

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

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

View File

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

View File

@ -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'

View File

@ -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",

View File

@ -387,59 +387,58 @@ export async function generateDocUpdateMessages (
return res
}
async function ActivityMessagesHandler (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
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<Doc>[], control: TriggerControl): Promise<Tx[]> {
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',
{},
async (ctx) => await generateDocUpdateMessages(ctx, tx, control, [], undefined, cache)
: await control.ctx.with('generateDocUpdateMessages', {}, (ctx) =>
generateDocUpdateMessages(ctx, tx, control, [], undefined, cache)
)
const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc<DocUpdateMessage>))
const notificationTxes = await control.ctx.with(
'createCollaboratorNotifications',
{},
async (ctx) =>
await createCollaboratorNotifications(ctx, tx, control, messages, undefined, cache.docs as Map<Ref<Doc>, Doc>)
const notificationTxes = await control.ctx.with('createCollaboratorNotifications', {}, (ctx) =>
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) {
await control.apply(control.ctx, result)
}
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>
if (tx._class !== core.class.TxRemoveDoc) {
return []
continue
}
const activityDocMixin = control.hierarchy.classHierarchyMixin(tx.objectClass, activity.mixin.ActivityDoc)
if (activityDocMixin === undefined) {
return []
continue
}
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 } }
)
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 (

View File

@ -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,26 +241,29 @@ async function onSupportWorkspaceMessage (control: TriggerControl, message: Chat
}
export async function OnMessageSend (
originTx: TxCollectionCUD<Doc, AttachedDoc>,
originTxs: TxCollectionCUD<Doc, AttachedDoc>[],
control: TriggerControl
): Promise<Tx[]> {
const { hierarchy } = control
const tx = TxProcessor.extractTx(originTx) as TxCreateDoc<ChatMessage>
if (tx._class !== core.class.TxCreateDoc || !hierarchy.isDerived(tx.objectClass, chunter.class.ChatMessage)) {
const txes = originTxs
.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 []
}
if (tx.modifiedBy === aiBot.account.AIBot || tx.modifiedBy === core.account.System) {
return []
}
for (const tx of txes) {
const isThread = hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage)
const message = TxProcessor.createDoc2Doc(tx)
const docClass = isThread ? (message as ThreadMessage).objectClass : message.attachedToClass
if (!hierarchy.isDerived(docClass, chunter.class.ChunterSpace)) {
return []
continue
}
if (docClass === chunter.class.DirectMessage) {
@ -270,6 +273,7 @@ export async function OnMessageSend (
if (docClass === analyticsCollector.class.OnboardingChannel) {
await onSupportWorkspaceMessage(control, message)
}
}
return []
}

View File

@ -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<Doc>, control: TriggerCon
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 actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<ChatMessage>
@ -163,9 +164,7 @@ async function OnChatMessageCreated (tx: TxCUD<Doc>, 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<Doc>, 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<Doc>, 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
}

View File

@ -13,28 +13,26 @@
// 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<Tx[]> {
const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc<Doc>
if (rmTx._class !== core.class.TxRemoveDoc) {
return []
}
const ltxes = txes
.map((it) => TxProcessor.extractTx(it))
.filter((it) => it._class === core.class.TxRemoveDoc) as TxRemoveDoc<Doc>[]
for (const rmTx of ltxes) {
// Obtain document being deleted
const doc = removedMap.get(rmTx.objectId)
if (doc === undefined) {
return []
continue
}
// 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
// and when editing session ends, the collborator service will recreate the document again
await removeCollaborativeDoc(storageAdapter, workspace, toDelete, ctx)
}
return []
}

View File

@ -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<PersonAccount>)
)
for (const target of targets) {
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
}
async function OnDocRemove (originTx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
const tx = TxProcessor.extractTx(originTx) as TxRemoveDoc<Doc>
if (tx._class !== core.class.TxRemoveDoc) return []
async function OnDocRemove (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
const ltxes = txes
.map((it) => TxProcessor.extractTx(it))
.filter((it) => it._class === core.class.TxRemoveDoc) as TxRemoveDoc<Doc>[]
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
@ -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(...txes)
res.push(...(await removeCollaboratorDoc(tx, control)))
}
return res
}

View File

@ -13,55 +13,51 @@
// 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<Tx[]> {
if (tx._class !== core.class.TxCollectionCUD) {
return []
}
export async function OnRequest (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
const ptx = tx as TxCollectionCUD<Doc, Request>
if (!hierarchy.isDerived(ptx.tx.objectClass, request.class.Request)) {
return []
}
const ltxes = (
txes.filter((it) => it._class === core.class.TxCollectionCUD) as TxCollectionCUD<Doc, Request>[]
).filter((it) => hierarchy.isDerived(it.tx.objectClass, request.class.Request))
let res: Tx[] = []
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))
}
}
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

View File

@ -49,7 +49,9 @@ export async function TagElementRemove (
/**
* @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 isCreate = control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)
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 (isCreate) {
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 }
})
return [res]
)
}
if (isRemove) {
const ctx = actualTx as TxRemoveDoc<TagReference>
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, {
result.push(
control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, {
$inc: { refCount: -1 }
})
return [res]
)
}
}
}
return []
}
return result
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type

View File

@ -20,31 +20,34 @@ import task, { Task } from '@hcengineering/task'
/**
* @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>
if (!control.hierarchy.isDerived(actualTx.objectClass, task.class.Task)) return []
if (actualTx._class === core.class.TxCreateDoc) {
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) {
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) {
const updateTx = actualTx as TxUpdateDoc<Task>
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) {
return [
result.push(
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
isDone: true
})
]
)
} else {
return [
result.push(
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
isDone: false
})
]
)
}
}
}
}

View File

@ -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 === '') {
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 []
}

View File

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

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 {
const name = this.conf.domains[domain] ?? '#default'
const adapter = this.adapters.get(name) ?? this.defaultAdapter

View File

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

View File

@ -17,7 +17,6 @@
import core, {
TxFactory,
TxProcessor,
generateId,
groupByArray,
matchQuery,
type Class,
@ -134,7 +133,6 @@ export class Triggers {
mode: 'sync' | 'async'
): Promise<Tx[]> {
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
}

View File

@ -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<void>

View File

@ -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: {

View File

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

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, {
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<SessionData>, txes: Tx[]): Promise<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) {
// 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(
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',
{ domain },
async () => await adapter.load(ctx, domain, toDelete),
{},
() =>
adapter.load(
ctx,
d,
docs.map((it) => it._id)
),
{ count: toDelete.length }
)
for (const ddoc of toDeleteDocs) {
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<Domain>()
for (const tx of txes) {
const txCUD = TxProcessor.extractTx(tx) as TxCUD<Doc>
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
}

View File

@ -89,9 +89,7 @@ export class LiveQueryMiddleware extends BaseMiddleware implements Middleware {
}
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)
}
}

View File

@ -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<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'
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<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
delete (options as ServerFindOptions<Doc>)?.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<SessionDataImpl>).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)
}
})
}
}
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 {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverGithub.trigger.OnProjectChanges,
arrays: true,
isAsync: true
})

View File

@ -29,17 +29,22 @@ import tracker from '@hcengineering/tracker'
/**
* @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)
if (ltx._class === core.class.TxMixin && (ltx as TxMixin<Doc, Doc>).mixin === github.mixin.GithubIssue) {
const mix = ltx as TxMixin<Doc, Doc>
// Do not spend time to wait for trigger processing
await updateDocSyncInfo(control, tx, mix.objectSpace, mix)
return []
await updateDocSyncInfo(control, tx, mix.objectSpace, mix, cache, toApply)
continue
}
if (control.hierarchy.isDerived(ltx._class, core.class.TxCUD)) {
if (TxProcessor.isExtendsCUD(ltx._class)) {
const cud = ltx as TxCUD<Doc>
let space: Ref<Space> = cud.objectSpace
@ -52,15 +57,14 @@ export async function OnProjectChanges (tx: Tx, control: TriggerControl): Promis
}
if (isDocSyncUpdateRequired(control.hierarchy, cud)) {
// Do not spend time to wait for trigger processing
await updateDocSyncInfo(control, tx, space, 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<Doc, AttachedDoc>
if (control.hierarchy.isDerived(coll.objectClass, github.class.GithubPullRequest)) {
// Ok we got todo change for pull request, let's mark it for sync.
return [
result.push(
control.txFactory.createTxUpdateDoc<DocSyncInfo>(
github.class.DocSyncInfo,
coll.objectSpace,
@ -69,12 +73,16 @@ export async function OnProjectChanges (tx: Tx, control: TriggerControl): Promis
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<Class<Tx>>
objectId: Ref<Doc>
objectClass: Ref<Class<Doc>>
}
},
cache: Map<string, any>,
toApply: Tx[]
): Promise<void> {
const checkTx = (tx: Tx): boolean =>
control.hierarchy.isDerived(tx._class, core.class.TxCUD) &&
(tx as TxCUD<Doc>).objectClass === github.class.DocSyncInfo &&
(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
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<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>
})
).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<Doc>): boolean {
)
}
async function updateSyncDoc (
function updateSyncDoc (
control: TriggerControl,
cud: {
_class: Ref<Class<Tx>>
@ -168,8 +187,9 @@ async function updateSyncDoc (
objectClass: Ref<Class<Doc>>
},
space: Ref<Space>,
info: DocSyncInfo
): Promise<void> {
info: DocSyncInfo,
toApply: Tx[]
): void {
const data: DocumentUpdate<DocSyncInfo> =
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<DocSyncInfo>(
github.class.DocSyncInfo,
info.space,
cud.objectId as Ref<DocSyncInfo>,
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<Class<Tx>>
@ -209,8 +229,9 @@ async function createSyncDoc (
objectClass: Ref<Class<Doc>>
},
tx: Tx,
space: Ref<Space>
): Promise<void> {
space: Ref<Space>,
toApply: Tx[]
): void {
const data: DocumentUpdate<DocSyncInfo> = {
url: '',
githubNumber: 0,
@ -226,14 +247,14 @@ async function createSyncDoc (
data.attachedTo = coll.objectId
}
await control.apply(control.ctx, [
toApply.push(
control.txFactory.createTxCreateDoc<DocSyncInfo>(
github.class.DocSyncInfo,
space,
data,
cud.objectId as Ref<DocSyncInfo>
)
])
)
control.ctx.contextData.broadcast.targets.github = (it) => {
if (control.hierarchy.isDerived(it._class, core.class.TxCUD)) {
if ((it as TxCUD<Doc>).objectClass === github.class.DocSyncInfo) {

View File

@ -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
@ -173,3 +176,12 @@ services:
- STORAGE_CONFIG=${STORAGE_CONFIG}
- REKONI_URL=http://rekoni:4007
- 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
./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

View File

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